阅读本文需要有一定的汇编和 C 语言基础,文章附带的源码实现的是解析器,不是加载器!内容较多,右上角有目录!

概要

PE(Portable Executable File Format)是目前 windows 平台上的主流可执行文件格式。早期的格式为 COFF 格式,后来微软参考了 COFF 格式,开发出了 PE 格式,COFF 是 Common Object File Format 的缩写,意为通用文件格式,是应用于数种 Unix 系统和 VMS 系统中的目标文件格式和可执行文件格式。

PE 文件结构大致布局

PE 文件使用的是一个平面地址空间,所有代码和数据都被合并在一起,组成一个很大的结构,文件的内容被分割为不同的区块,区块包含代码和数据,各个区块按页边界来对齐,区块没有大小限制,是一个连续结构,每个块都有它自己在内存中的一套属性。PE 文件是由 PE 加载器加载到内存中的,这个 PE 加载器也就是 windows 加载器,它并不是将 PE 文件作为单一内存映射文件装入到内存中,而是去遍历 PE 文件,决定将哪一部分进行映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址,当磁盘文件装入到内存中,其数据结构布局是一致的,但是数据之间的相对位置可能会改变

数据之间的相对位置

MS-DOS 头

PE 文件中的 MS-DOS 头是微软出于兼容性考虑而保留下来的,MS-DOS 头也称为 DOS 头,紧接它后面的是 DOS Stub,当 PE 文件运行在 MS-DOS 系统时,会执行 DOS Stub 中的内容,不会出现不可预料的错误。

DOS 头结构在 winnt.h 中有定义,是一个 64 (0x40) 字节长的结构体

typedef struct _IMAGE_DOS_HEADER {
    WORD   e_magic;   //[0x00] 1)  MZ 标志位
    WORD   e_cblp;    //[0x02] 2)  文件末页字节数
    WORD   e_cp;      //[0x04] 3)  文件页数
    WORD   e_crlc;    //[0x06] 4)  重定位
    WORD   e_cparhdr; //[0x08] 5)  区段中头部大小
    WORD   e_minalloc;//[0x0A] 6)  最小附加内存段需求
    WORD   e_maxalloc;//[0x0C] 7)  最大附加内存段需求
    WORD   e_ss;      //[0x0E] 8)  初始 SS 值
    WORD   e_sp;      //[0x10] 9)  初始 SP 值
    WORD   e_csum;    //[0x12] 10) 校验和
    WORD   e_ip;      //[0x14] 11) 初始 IP 值
    WORD   e_cs;      //[0x16] 12) 初始 CS 值
    WORD   e_lfarlc;  //[0x18] 13) 重定位表偏移
    WORD   e_ovno;    //[0x1A] 14) 附加数
    WORD   e_res[4];  //[0x1C] 15) 保留
    WORD   e_oemid;   //[0x24] 16) OEM 标识
    WORD   e_oeminfo; //[0x26] 17) OEM 信息
    WORD   e_res2[10];//[0x28] 18) 保留
    LONG   e_lfanew;  //[0x3C] 19) 新文件头地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

结构体中有 19 个字段,前 18 个字段为 WORD 型 (2 字节),其中有 2 个为 WORD 型数组,共占 60 字节,十六进制表示为 0x3C (e_lfanew 开始偏移处)。大部分字段对 MS-DOS 系统中很重要,如果想在 MS-DOS 下执行一些操作,那就必须全面了解,但在 Windows 系统中就只需要了解 3 个字段就足够了,一个是 e_magic,一个是 e_lfanew,次重要字段为 e_lfarlc:

  • e_magic:这是结构体中第一个字段,类型为 WORD,占用 2 个字节,称为魔数,为固定值 0x5A4D,用 IMAGE_DOS_SIGNATURE 宏表示,该字段表明这是一个 MS-DOS 下的可执行文件;
  • e_lfanew:PE 头的真正偏移,可进一步判断这个可执行文件是否为 PE 文件;
  • e_lfarlc:重定位表偏移,意为 DOS Stub 的起始偏移地址,在 DOS Stub 开始之处往前推 4 个字节便是指向 e_lfanew 的地址,但大多数人只记住 e_lfanew 的偏移地址 0x3C;

现在就来实际看一下吧,推荐使用 010Editor (https://www.sweetscape.com/010editor/),这是一款功能强大的十六进制编辑软件,打开 PE 文件后会自动加载 PE 解析模板然后自动解析结构

010 Editor 自动解析 PE 结构

相关字段偏移说明在图中已标出,右边红框框住的一大块为 DOS Stub,需要注意的是,以 0x80 处分界后半部分严格来说是不属于 DOS Stub 的,也不属于 PE 的,它有一个名称,名为 Rich Header,前半部分里面包含了一个字符串 This is program cannot be run in DOS mode,表示该程序不能在 DOS 下运行。现将前半部分内容另存为一个 bin 文件

0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68
69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F
74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20
6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00

用 IDA 打开

IDA 打开 bin 文件

选择 No (16 位模式,DOS 程序都是 16 位),反编译后为

bin 文件反编译后的代码

汇编代码

push cs
pop ds
mov dx, 0Eh
mov ah, 9
int 21h
mov ax, 4C01h
int 21h

前两句表示将 cs 的值放入到 ds 中,这只是将代码段和数据段数值设置相同的一种方式。第 3 句将 dx 的值设为 0Eh,表示字符串 This program cannot be run in DOS mode. 的地址,第 4 句将 ah 设置为 9,第 5 句调用 DOS 中断 21h 服务,它需要一个参数来确定要执行的函数,该参数位于 ah 中,9 代表将字符串打印到屏幕,打印的字符串地址是存放在 dx 中,不了解 DOS 中断方面的可以参考中文维基百科关于 DOS API 说明:https://zh.wikipedia.org/wiki/DOS_API

最后一句再次调用 DOS 中断 21h 服务,前一句设置 ax 的值为 0x4C01 (ah 为 0x4C,al 为 0x01),0x4C 表示用指定返回代码终止,返回代码为 0x01。DOS Stub 作用是为了将一条错误信息打印至屏幕上并退出,仅此而已。

网上有很多对 PE 结构解析的文章,并未提到 Rich Header,甚至上面的内容也很少提及,可能认为不太重要吧。虽不太重要,但不妨可以了解一下。

现在来说说 Rich Header,它存在于 DOS Stub 和 NT Header 之间,是一种未公开的结构,以 DanS 开头,并以 Rich 字符串,后跟一个检验和 (checksum,一个用于异或的 key) 结尾,存在于使用 Visual Studio 工具集构建的可执行文件中,表示的是构建工具集的一些元数据,这些元数据包括类型、特定版本及构建号,把这部分数据全抹为 0 并不会影响文件的执行。

这部分内容进行了异或加密,首先 rich id 为 0x68636952,表示的是 Rich,后跟一个检验和,会用此校验和当作异或 key,值为 0x48396E4C

字段解析

开头 0x1B570F08 跟 xor_key 进行异或得到的是一个开始标志 DanS,后面三条红杠标出的进行异或为 0,为 3 个清零的 DWORD 型数据。

xor 后结果

后面跟着 11 条黄杠标出的数据 (其实有 12 条,最后一条全为 0,表示结束),有效的为 11 项,每项包含 2 个 DWORD 型数据,将这两个数据分别与 xor_key 进行异或,比如第一条黄杠标出的数据

07 16 3A 49 47 6E 39 48
--------------------------
    0x48396E47 0x493A1607
xor 0x48396E4C 0x48396E4C
--------------------------
    0xB        0x103784B
--------------------------
      B 103 784B
--------------------------
      11.259.30795

11 表示的工具生成的项目数,比如一个应用程序由 3 个 C++ 源文件组成,那么项目计数为 3,259 表示的是工具标识符,比如 C++ 编译器、C 编译器、资源编译器、MASM 等,此处对应为 Masm1400 (Visual Studio 2015 14.00),30795 表示的是构建的 ID。可能大家觉得研究这个没有啥用,但是在实际当中有攻击者使用过此结构,参考卡巴斯基这篇报告:https://securelist.com/the-devils-in-the-rich-header/84348/

本篇报告中对于 Rich Header 研究部分大致概括为 OlympicDestroyer 恶意软件作者修改了 Rich Header,使其看起来和 Bluenoroff 相同,卡巴斯基安全研究人员通过反复对比不同构建工具集构建出来的可执行文件 Rich Header,并结合 OlympicDestroyer 和 Bluenoroff,发现 OlympicDestroyer 的 Rich Header 是假的,是从 Bluenoroff 复制过来,研究人员表示不能够理解这一动机,但能肯定 OlympicDestroyer 的创建者有意修改了此结构,使其与 Lazarus 组织制作的 Bluenoroff 样本相似。

感兴趣的可以用写个解析 Rich Header 的小工具,以下是我写的小 Demo

RichHeader.h

#pragma once

#include <Windows.h>
#include <stdint.h>

#define RICH_SIGNATURE 0x68636952
#define DANS_SIGNATURE 0x536E6144

typedef struct _RICH_HEADER_ENTRY {
	DWORD dwIdVersion;
	DWORD dwCount;
} RICH_HEADER_ENTRY, *PRICH_HEADER_ENTRY;

typedef struct _RICH_HEADER {
	DWORD dwStartMarker;
	DWORD dwPadding[3];
	PRICH_HEADER_ENTRY pEntries;
	INT entryCount;
	DWORD dwEndMarker;
	DWORD dwXorKey;
} RICH_HEADER, *PRICH_HEADER;

const char* GetToolNameByID(WORD id);
const char* GetVSNameByID(WORD id);

INT ParserRichHeader(PIMAGE_DOS_HEADER pDosHeader, PUINT8 buffer);

RichHeader.c

#include "RichHeader.h"
#include <stdio.h>

// 解析 Rich Header
INT ParserRichHeader(PIMAGE_DOS_HEADER pDosHeader, PUINT8 buffer) {
    RICH_HEADER richHeader = { 0 };
    richHeader.dwStartMarker = DANS_SIGNATURE;
    richHeader.dwEndMarker = RICH_SIGNATURE;

    DWORD richOffset = 0;
    for (DWORD i = sizeof(IMAGE_DOS_HEADER); i < (DWORD)pDosHeader->e_lfanew - 8; i++) {
        DWORD sig = *(DWORD*)(buffer + i);
        if (sig == RICH_SIGNATURE) {
            richOffset = i;
            richHeader.dwXorKey = *(DWORD*)(buffer + i + 4);
            break;
        }
    }

    DWORD dansOffset = 0;
    for (DWORD i = richOffset - 4; i >= sizeof(IMAGE_DOS_HEADER); i -= 4) {
        DWORD val = *(DWORD*)(buffer + i) ^ richHeader.dwXorKey;
        if (val == DANS_SIGNATURE) {
            dansOffset = i;
            break;
        }
    }

    richHeader.entryCount = (richOffset - dansOffset - 16) / 8;

    richHeader.pEntries = (PRICH_HEADER_ENTRY)malloc(sizeof(PRICH_HEADER_ENTRY) * richHeader.entryCount);
    if (!richHeader.pEntries) {
        printf("内存分配失败\n");
        free(buffer);
        return 0;
    }

    for (int i = 0; i < richHeader.entryCount; i++) {
        DWORD off = dansOffset + 16 + i * 8;
        richHeader.pEntries[i].dwIdVersion = *(DWORD*)(buffer + off) ^ richHeader.dwXorKey;
        richHeader.pEntries[i].dwCount = *(DWORD*)(buffer + off + 4) ^ richHeader.dwXorKey;
    }

    printf("==================== Rich Header ====================\n");
    printf("XOR Key: 0x%08X\n", richHeader.dwXorKey);
    printf("Entry Count: %d\n\n", richHeader.entryCount);

    for (int i = 0; i < richHeader.entryCount; i++) {
        WORD wToolId = richHeader.pEntries[i].dwIdVersion >> 16;
        WORD wVersion = richHeader.pEntries[i].dwIdVersion & 0xFFFF;
        printf("Entry %02d: ToolID=0x%04X (%-16s), Build=0x%04X (%5d), Count=%4u, VSVersion=%s\n",
            i + 1, wToolId, GetToolNameByID(wToolId),
            wVersion, wVersion, richHeader.pEntries[i].dwCount,
            GetVSNameByID(wToolId));
    }

    return 1;
}

// https://github.com/kirschju/richheader
const char* GetVSNameByID(WORD id) {
    if (id >= 0x0106 && id < (0x010a + 1))
        return "Visual Studio 2017 14.01+";
    if (id >= 0x00fd && id < (0x0106))
        return "Visual Studio 2015 14.00";
    if (id >= 0x00eb && id < 0x00fd)
        return "Visual Studio 2013 12.10";
    if (id >= 0x00d9 && id < 0x00eb)
        return "Visual Studio 2013 12.00";
    if (id >= 0x00c7 && id < 0x00d9)
        return "Visual Studio 2012 11.00";
    if (id >= 0x00b5 && id < 0x00c7)
        return "Visual Studio 2010 10.10";
    if (id >= 0x0098 && id < 0x00b5)
        return "Visual Studio 2010 10.00";
    if (id >= 0x0083 && id < 0x0098)
        return "Visual Studio 2008 09.00";
    if (id >= 0x006d && id < 0x0083)
        return "Visual Studio 2005 08.00";
    if (id >= 0x005a && id < 0x006d)
        return "Visual Studio 2003 07.10";
    if (id >= 0x0019 && id < (0x0045 + 1))
        return "Visual Studio 2002 07.00";
    if (id == 0xA || id == 0xB || id == 0xD || id == 0x15 || id == 0x16)
        return "Visual Studio 6.0 06.00";
    if (id == 0x2 || id == 0x6 || id == 0xC || id == 0xE)
        return "Visual Studio 97 05.00";
    if (id == 1)
        return "Visual Studio";
    return "null";
}

// https://github.com/kirschju/richheader
const char* GetToolNameByID(WORD id) {
    switch (id) {
    case 0x0000: return "Unknown";
    case 0x0001: return "Import0";
    case 0x0002: return "Linker510";
    case 0x0003: return "Cvtomf510";
    case 0x0004: return "Linker600";
    case 0x0005: return "Cvtomf600";
    case 0x0006: return "Cvtres500";
    case 0x0007: return "Utc11_Basic";
    case 0x0008: return "Utc11_C";
    case 0x0009: return "Utc12_Basic";
    case 0x000a: return "Utc12_C";
    case 0x000b: return "Utc12_CPP";
    case 0x000c: return "AliasObj60";
    case 0x000d: return "VisualBasic60";
    case 0x000e: return "Masm613";
    case 0x000f: return "Masm710";
    case 0x0010: return "Linker511";
    case 0x0011: return "Cvtomf511";
    case 0x0012: return "Masm614";
    case 0x0013: return "Linker512";
    case 0x0014: return "Cvtomf512";
    case 0x0015: return "Utc12_C_Std";
    case 0x0016: return "Utc12_CPP_Std";
    case 0x0017: return "Utc12_C_Book";
    case 0x0018: return "Utc12_CPP_Book";
    case 0x0019: return "Implib700";
    case 0x001a: return "Cvtomf700";
    case 0x001b: return "Utc13_Basic";
    case 0x001c: return "Utc13_C";
    case 0x001d: return "Utc13_CPP";
    case 0x001e: return "Linker610";
    case 0x001f: return "Cvtomf610";
    case 0x0020: return "Linker601";
    case 0x0021: return "Cvtomf601";
    case 0x0022: return "Utc12_1_Basic";
    case 0x0023: return "Utc12_1_C";
    case 0x0024: return "Utc12_1_CPP";
    case 0x0025: return "Linker620";
    case 0x0026: return "Cvtomf620";
    case 0x0027: return "AliasObj70";
    case 0x0028: return "Linker621";
    case 0x0029: return "Cvtomf621";
    case 0x002a: return "Masm615";
    case 0x002b: return "Utc13_LTCG_C";
    case 0x002c: return "Utc13_LTCG_CPP";
    case 0x002d: return "Masm620";
    case 0x002e: return "ILAsm100";
    case 0x002f: return "Utc12_2_Basic";
    case 0x0030: return "Utc12_2_C";
    case 0x0031: return "Utc12_2_CPP";
    case 0x0032: return "Utc12_2_C_Std";
    case 0x0033: return "Utc12_2_CPP_Std";
    case 0x0034: return "Utc12_2_C_Book";
    case 0x0035: return "Utc12_2_CPP_Book";
    case 0x0036: return "Implib622";
    case 0x0037: return "Cvtomf622";
    case 0x0038: return "Cvtres501";
    case 0x0039: return "Utc13_C_Std";
    case 0x003a: return "Utc13_CPP_Std";
    case 0x003b: return "Cvtpgd1300";
    case 0x003c: return "Linker622";
    case 0x003d: return "Linker700";
    case 0x003e: return "Export622";
    case 0x003f: return "Export700";
    case 0x0040: return "Masm700";
    case 0x0041: return "Utc13_POGO_I_C";
    case 0x0042: return "Utc13_POGO_I_CPP";
    case 0x0043: return "Utc13_POGO_O_C";
    case 0x0044: return "Utc13_POGO_O_CPP";
    case 0x0045: return "Cvtres700";
    case 0x0046: return "Cvtres710p";
    case 0x0047: return "Linker710p";
    case 0x0048: return "Cvtomf710p";
    case 0x0049: return "Export710p";
    case 0x004a: return "Implib710p";
    case 0x004b: return "Masm710p";
    case 0x004c: return "Utc1310p_C";
    case 0x004d: return "Utc1310p_CPP";
    case 0x004e: return "Utc1310p_C_Std";
    case 0x004f: return "Utc1310p_CPP_Std";
    case 0x0050: return "Utc1310p_LTCG_C";
    case 0x0051: return "Utc1310p_LTCG_CPP";
    case 0x0052: return "Utc1310p_POGO_I_C";
    case 0x0053: return "Utc1310p_POGO_I_CPP";
    case 0x0054: return "Utc1310p_POGO_O_C";
    case 0x0055: return "Utc1310p_POGO_O_CPP";
    case 0x0056: return "Linker624";
    case 0x0057: return "Cvtomf624";
    case 0x0058: return "Export624";
    case 0x0059: return "Implib624";
    case 0x005a: return "Linker710";
    case 0x005b: return "Cvtomf710";
    case 0x005c: return "Export710";
    case 0x005d: return "Implib710";
    case 0x005e: return "Cvtres710";
    case 0x005f: return "Utc1310_C";
    case 0x0060: return "Utc1310_CPP";
    case 0x0061: return "Utc1310_C_Std";
    case 0x0062: return "Utc1310_CPP_Std";
    case 0x0063: return "Utc1310_LTCG_C";
    case 0x0064: return "Utc1310_LTCG_CPP";
    case 0x0065: return "Utc1310_POGO_I_C";
    case 0x0066: return "Utc1310_POGO_I_CPP";
    case 0x0067: return "Utc1310_POGO_O_C";
    case 0x0068: return "Utc1310_POGO_O_CPP";
    case 0x0069: return "AliasObj710";
    case 0x006a: return "AliasObj710p";
    case 0x006b: return "Cvtpgd1310";
    case 0x006c: return "Cvtpgd1310p";
    case 0x006d: return "Utc1400_C";
    case 0x006e: return "Utc1400_CPP";
    case 0x006f: return "Utc1400_C_Std";
    case 0x0070: return "Utc1400_CPP_Std";
    case 0x0071: return "Utc1400_LTCG_C";
    case 0x0072: return "Utc1400_LTCG_CPP";
    case 0x0073: return "Utc1400_POGO_I_C";
    case 0x0074: return "Utc1400_POGO_I_CPP";
    case 0x0075: return "Utc1400_POGO_O_C";
    case 0x0076: return "Utc1400_POGO_O_CPP";
    case 0x0077: return "Cvtpgd1400";
    case 0x0078: return "Linker800";
    case 0x0079: return "Cvtomf800";
    case 0x007a: return "Export800";
    case 0x007b: return "Implib800";
    case 0x007c: return "Cvtres800";
    case 0x007d: return "Masm800";
    case 0x007e: return "AliasObj800";
    case 0x007f: return "PhoenixPrerelease";
    case 0x0080: return "Utc1400_CVTCIL_C";
    case 0x0081: return "Utc1400_CVTCIL_CPP";
    case 0x0082: return "Utc1400_LTCG_MSIL";
    case 0x0083: return "Utc1500_C";
    case 0x0084: return "Utc1500_CPP";
    case 0x0085: return "Utc1500_C_Std";
    case 0x0086: return "Utc1500_CPP_Std";
    case 0x0087: return "Utc1500_CVTCIL_C";
    case 0x0088: return "Utc1500_CVTCIL_CPP";
    case 0x0089: return "Utc1500_LTCG_C";
    case 0x008a: return "Utc1500_LTCG_CPP";
    case 0x008b: return "Utc1500_LTCG_MSIL";
    case 0x008c: return "Utc1500_POGO_I_C";
    case 0x008d: return "Utc1500_POGO_I_CPP";
    case 0x008e: return "Utc1500_POGO_O_C";
    case 0x008f: return "Utc1500_POGO_O_CPP";
    case 0x0090: return "Cvtpgd1500";
    case 0x0091: return "Linker900";
    case 0x0092: return "Export900";
    case 0x0093: return "Implib900";
    case 0x0094: return "Cvtres900";
    case 0x0095: return "Masm900";
    case 0x0096: return "AliasObj900";
    case 0x0097: return "Resource";
    case 0x0098: return "AliasObj1000";
    case 0x0099: return "Cvtpgd1600";
    case 0x009a: return "Cvtres1000";
    case 0x009b: return "Export1000";
    case 0x009c: return "Implib1000";
    case 0x009d: return "Linker1000";
    case 0x009e: return "Masm1000";
    case 0x009f: return "Phx1600_C";
    case 0x00a0: return "Phx1600_CPP";
    case 0x00a1: return "Phx1600_CVTCIL_C";
    case 0x00a2: return "Phx1600_CVTCIL_CPP";
    case 0x00a3: return "Phx1600_LTCG_C";
    case 0x00a4: return "Phx1600_LTCG_CPP";
    case 0x00a5: return "Phx1600_LTCG_MSIL";
    case 0x00a6: return "Phx1600_POGO_I_C";
    case 0x00a7: return "Phx1600_POGO_I_CPP";
    case 0x00a8: return "Phx1600_POGO_O_C";
    case 0x00a9: return "Phx1600_POGO_O_CPP";
    case 0x00aa: return "Utc1600_C";
    case 0x00ab: return "Utc1600_CPP";
    case 0x00ac: return "Utc1600_CVTCIL_C";
    case 0x00ad: return "Utc1600_CVTCIL_CPP";
    case 0x00ae: return "Utc1600_LTCG_C";
    case 0x00af: return "Utc1600_LTCG_CPP";
    case 0x00b0: return "Utc1600_LTCG_MSIL";
    case 0x00b1: return "Utc1600_POGO_I_C";
    case 0x00b2: return "Utc1600_POGO_I_CPP";
    case 0x00b3: return "Utc1600_POGO_O_C";
    case 0x00b4: return "Utc1600_POGO_O_CPP";
    case 0x00b5: return "AliasObj1010";
    case 0x00b6: return "Cvtpgd1610";
    case 0x00b7: return "Cvtres1010";
    case 0x00b8: return "Export1010";
    case 0x00b9: return "Implib1010";
    case 0x00ba: return "Linker1010";
    case 0x00bb: return "Masm1010";
    case 0x00bc: return "Utc1610_C";
    case 0x00bd: return "Utc1610_CPP";
    case 0x00be: return "Utc1610_CVTCIL_C";
    case 0x00bf: return "Utc1610_CVTCIL_CPP";
    case 0x00c0: return "Utc1610_LTCG_C";
    case 0x00c1: return "Utc1610_LTCG_CPP";
    case 0x00c2: return "Utc1610_LTCG_MSIL";
    case 0x00c3: return "Utc1610_POGO_I_C";
    case 0x00c4: return "Utc1610_POGO_I_CPP";
    case 0x00c5: return "Utc1610_POGO_O_C";
    case 0x00c6: return "Utc1610_POGO_O_CPP";
    case 0x00c7: return "AliasObj1100";
    case 0x00c8: return "Cvtpgd1700";
    case 0x00c9: return "Cvtres1100";
    case 0x00ca: return "Export1100";
    case 0x00cb: return "Implib1100";
    case 0x00cc: return "Linker1100";
    case 0x00cd: return "Masm1100";
    case 0x00ce: return "Utc1700_C";
    case 0x00cf: return "Utc1700_CPP";
    case 0x00d0: return "Utc1700_CVTCIL_C";
    case 0x00d1: return "Utc1700_CVTCIL_CPP";
    case 0x00d2: return "Utc1700_LTCG_C";
    case 0x00d3: return "Utc1700_LTCG_CPP";
    case 0x00d4: return "Utc1700_LTCG_MSIL";
    case 0x00d5: return "Utc1700_POGO_I_C";
    case 0x00d6: return "Utc1700_POGO_I_CPP";
    case 0x00d7: return "Utc1700_POGO_O_C";
    case 0x00d8: return "Utc1700_POGO_O_CPP";
    case 0x00d9: return "AliasObj1200";
    case 0x00da: return "Cvtpgd1800";
    case 0x00db: return "Cvtres1200";
    case 0x00dc: return "Export1200";
    case 0x00dd: return "Implib1200";
    case 0x00de: return "Linker1200";
    case 0x00df: return "Masm1200";
    case 0x00e0: return "Utc1800_C";
    case 0x00e1: return "Utc1800_CPP";
    case 0x00e2: return "Utc1800_CVTCIL_C";
    case 0x00e3: return "Utc1800_CVTCIL_CPP";
    case 0x00e4: return "Utc1800_LTCG_C";
    case 0x00e5: return "Utc1800_LTCG_CPP";
    case 0x00e6: return "Utc1800_LTCG_MSIL";
    case 0x00e7: return "Utc1800_POGO_I_C";
    case 0x00e8: return "Utc1800_POGO_I_CPP";
    case 0x00e9: return "Utc1800_POGO_O_C";
    case 0x00ea: return "Utc1800_POGO_O_CPP";
    case 0x00eb: return "AliasObj1210";
    case 0x00ec: return "Cvtpgd1810";
    case 0x00ed: return "Cvtres1210";
    case 0x00ee: return "Export1210";
    case 0x00ef: return "Implib1210";
    case 0x00f0: return "Linker1210";
    case 0x00f1: return "Masm1210";
    case 0x00f2: return "Utc1810_C";
    case 0x00f3: return "Utc1810_CPP";
    case 0x00f4: return "Utc1810_CVTCIL_C";
    case 0x00f5: return "Utc1810_CVTCIL_CPP";
    case 0x00f6: return "Utc1810_LTCG_C";
    case 0x00f7: return "Utc1810_LTCG_CPP";
    case 0x00f8: return "Utc1810_LTCG_MSIL";
    case 0x00f9: return "Utc1810_POGO_I_C";
    case 0x00fa: return "Utc1810_POGO_I_CPP";
    case 0x00fb: return "Utc1810_POGO_O_C";
    case 0x00fc: return "Utc1810_POGO_O_CPP";
    case 0x00fd: return "AliasObj1400";
    case 0x00fe: return "Cvtpgd1900";
    case 0x00ff: return "Cvtres1400";
    case 0x0100: return "Export1400";
    case 0x0101: return "Implib1400";
    case 0x0102: return "Linker1400";
    case 0x0103: return "Masm1400";
    case 0x0104: return "Utc1900_C";
    case 0x0105: return "Utc1900_CPP";
    case 0x0106: return "Utc1900_CVTCIL_C";
    case 0x0107: return "Utc1900_CVTCIL_CPP";
    case 0x0108: return "Utc1900_LTCG_C";
    case 0x0109: return "Utc1900_LTCG_CPP";
    case 0x010a: return "Utc1900_LTCG_MSIL";
    case 0x010b: return ": 'Utc1900_POGO_I_C";
    case 0x010c: return "Utc1900_POGO_I_CPP";
    case 0x010d: return "Utc1900_POGO_O_C";
    case 0x010e: return "Utc1900_POGO_O_CPP";
    default: return "Unknown Tool";
    }
}

PEParser.c

#include "RichHeader.h"
#include <stdio.h>

PUINT8 GetFileContent(const char* filePath) {
	FILE* fp = fopen(filePath, "rb");
	if (!fp) {
		printf("打开文件失败\n");
		return NULL;
	}

	fseek(fp, 0, SEEK_END);
	long fileSize = ftell(fp);
	rewind(fp);

	PUINT8 buffer = (PUINT8)calloc(fileSize, sizeof(UINT8));
	if (!buffer) {
		printf("内存分配失败\n");
		fclose(fp);
		return NULL;
	}

	size_t bytesRead = fread(buffer, 1, fileSize, fp);
	if (bytesRead != fileSize) {
		printf("文件读取失败\n");
		free(buffer);
		fclose(fp);
		return NULL;
	}

	fclose(fp);

	return buffer;
}

int main(int argc, char* argv[]) {
	if (argc != 2) {
		printf("Usage: %s <pe_file>\n", argv[0]);
		return 1;
	}

	PUINT8 buffer = GetFileContent(argv[1]);
	if (buffer == NULL) {
		return 1;
	}

	// 解析 DOS 头,判断是否为有效的 PE 文件
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;
	if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
		printf("不是有效的 PE 文件\n");
		free(buffer);
		return 1;
	}

	PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(buffer + pDosHeader->e_lfanew);
	if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
		printf("不是有效的 PE 文件\n");
		free(buffer);
		return 1;
	}

	// 解析 Rich Header
	if (!ParserRichHeader(pDosHeader, buffer)) {
		printf("解析 Rich 头失败\n");
	}

	free(buffer);

	return 0;
}

运行结果

解析 Rich 头数据

NT 头

紧跟 Rich Header 后面就是 NT 头了,在说 NT 头之前,先说下 RVA 这个概念,RVA 意为相对虚拟地址,它是 Relative VirualAddress 的缩写,这个相对是相对于文件载入到内存后的基地址而言,后续提到某个字段的 RVA,表示这个字段在内存中相对于载入的基地址偏移大小,可计算出这个字段的虚拟地址 (VA) 为载入的内存基地址加上 RVA。NT 头结构包含 3 个字段:

// 32 位
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                            // [0x00] PE 标识
    IMAGE_FILE_HEADER FileHeader;               // [0x04] 文件头
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;     // [0x18] 可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

// 64 位
typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;                            // [0x00] PE 标识
    IMAGE_FILE_HEADER FileHeader;               // [0x04] 文件头
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;     // [0x18] 可选头
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

#ifdef _WIN64
typedef IMAGE_NT_HEADERS64                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64                 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32                 PIMAGE_NT_HEADERS;
#endif

文件布局 (32 位),后面的解析以 32 位来说明

NT 头数据

Signature

Signature 字段是一个 DWORD 型字段,占用 4 个字节,为固定值 0x00004550 (PE00),用 IMAGE_NT_SIGNATURE 宏表示,可用于进一步判断是否为 PE 文件。

PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(buffer + pDosHeader->e_lfanew);
if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
	printf("不是有效的 PE 文件\n");
	free(buffer);
	return 1;
}

文件头

第 2 个字段为文件头,是一个结构体,包含 7 个字段,共占 20 个字节,后面只说相对重要字段:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;                    //[0x04] 1)运行平台
    WORD    NumberOfSections;           //[0x06] 2)区段的数量
    DWORD   TimeDateStamp;              //[0x08] 3)文件的创建时间
    DWORD   PointerToSymbolTable;       //[0x0C] 4)符号表指针 (0,已弃用)
    DWORD   NumberOfSymbols;            //[0x10] 5)符号的数量 (0,已弃用)
    WORD    SizeOfOptionalHeader;       //[0x14] 6)扩展头大小
    WORD    Characteristics;            //[0x16] 7)文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

IMAGE_FILE_HEADER 结构体包含了 PE 文件的概览信息。Machine 字段表示此 PE 文件可以运行在哪种结构类型的 CPU 上,在给出的示例中为 0x14C,表示的是 Intel386,用 IMAGE_FILE_MACHINE_I386 宏表示,关于其它值可参考下面这张图 (具体类型可参看文档 https://learn.microsoft.com/zh-cn/windows/win32/debug/pe-format#machine-types):

Machine 可能取的值

NumberOfSections 字段表示的区段的数量,此示例为 5,表示有 5 个区段 (区段也称为节):

TimeDateStamp 字段表示文件创建的时间,此示例为 0x65E7FA12,需要将其转换为友好可读形式 (需包含 time.h 头文件):

#include <time.h>

// 后来补充
CONST CHAR* CharacterTrans(WORD wID) {
	switch (wID) {
	case IMAGE_FILE_RELOCS_STRIPPED: return "不包含重定位信息";
	case IMAGE_FILE_EXECUTABLE_IMAGE: return "可执行";
	case IMAGE_FILE_LINE_NUMS_STRIPPED: return "不包含行号信息";
	case IMAGE_FILE_LOCAL_SYMS_STRIPPED: return "不包含符号信息";
	case IMAGE_FILE_AGGRESIVE_WS_TRIM: return "强制缩减工作组";
	case IMAGE_FILE_LARGE_ADDRESS_AWARE: return "能处理超过 2GB 范围地址";
	case IMAGE_FILE_BYTES_REVERSED_LO: return "CPU 低字节颠倒";
	case IMAGE_FILE_32BIT_MACHINE: return "运行于 32 位平台";
	case IMAGE_FILE_DEBUG_STRIPPED: return "不包含 .DBG 文件调试信息";
	case IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP: return "在交换区运行";
	case IMAGE_FILE_NET_RUN_FROM_SWAP: return "在交换区运行";
	case IMAGE_FILE_SYSTEM: return "系统文件";
	case IMAGE_FILE_DLL: return "DLL 文件";
	case IMAGE_FILE_UP_SYSTEM_ONLY: return "只能运行于单处理器环境中";
	case IMAGE_FILE_BYTES_REVERSED_HI: return "CPU 高字节颠倒";
	default: return "";
	}
}

VOID ParseNTHeader(PIMAGE_NT_HEADERS pNtHeaders) {
	printf("==================== Nt Header ====================\n");
	PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;
	// 该应用程序运行在哪个架构上
	// https://learn.microsoft.com/zh-cn/windows/win32/debug/pe-format#machine-types
	switch (pFileHeader->Machine) {
	case IMAGE_FILE_MACHINE_I386:
		printf("Machine: Intel386\n");
		break;
	case IMAGE_FILE_MACHINE_AMD64:
		printf("Machine: AMD64\n");
		break;
	case IMAGE_FILE_MACHINE_R3000 || IMAGE_FILE_MACHINE_R4000 || IMAGE_FILE_MACHINE_R10000:
		printf("Machine: MIPS little-endian\n");
		break;
	case IMAGE_FILE_MACHINE_WCEMIPSV2:
		printf("Machine: MIPS little-endian WCE v2\n");
		break;
	case IMAGE_FILE_MACHINE_ALPHA:
		printf("Machine: Alpha_AXP\n");
		break;
	case IMAGE_FILE_MACHINE_POWERPC:
		printf("Machine: IBM PowerPC Little-Endian\n");
		break;
	case IMAGE_FILE_MACHINE_ARM:
		printf("Machine: ARM Little-Endian\n");
		break;
	case IMAGE_FILE_MACHINE_IA64:
		printf("Machine: Intel 64\n");
		break;
	case IMAGE_FILE_MACHINE_ALPHA64:
		printf("Machine: Alpha64\n");
		break;
	default:
		printf("Machine: Unknown\n");
		break;
	}

	printf("NumberOfSections: %d\n", pFileHeader->NumberOfSections);

	struct tm FileCreateTime;
	errno_t ret = gmtime_s(&FileCreateTime, (time_t*)&pFileHeader->TimeDateStamp);
	if (ret == 0) {
		printf("File CreateTime: %d-%d-%d %d:%d:%d\n",
			FileCreateTime.tm_year + 1900,
			FileCreateTime.tm_mon + 1,
			FileCreateTime.tm_mday,
			FileCreateTime.tm_hour + 8,
			FileCreateTime.tm_min,
			FileCreateTime.tm_sec);
	}

	printf("SizeOfOptionalHeader: 0x%04X\n", pFileHeader->SizeOfOptionalHeader);
    // printf("File Attribution: 0x%04X\n", pFileHeader->Characteristics);

    // 后来补充
	WORD w[4] = { 0 };
    w[0] = pFileHeader->Characteristics & 0xF;
    w[1] = pFileHeader->Characteristics & 0xF0;
    w[2] = pFileHeader->Characteristics & 0xF00;
    w[3] = pFileHeader->Characteristics & 0xF000;
    printf("File Attribution: 0x%04X -> ", pFileHeader->Characteristics);
    for (int i = 0; i < 4; i++) {
	    if (w[i] != 0) {
		    printf("[%s]", CharacterTrans(w[i]));
	    }
    }
    printf("\n");
}

运行结果 (参照 DIE):

运行结果与 DIE 比较
注意:程序的编译时间可以伪造

SizeOfOptionalHeader 字段表示扩展头的大小,一般为固定值,32 位为 0x00E0,64 位为 0x00F0。

Characteristics 字段表示 PE 文件的属性,可参考如下图:

Characteristics 字段可能表示的值

在此示例中为 0x2102,可看作是 0x2000 | 0x0100 | 0x0002 组合成的值,意为此示例为 32 位且可执行的 DLL 文件 (因为用的是 kernel32.dll)。具体更加详细说明参考:https://learn.microsoft.com/zh-cn/windows/win32/debug/pe-format#characteristics

可选头

NT 头中第 3 个字段为可选头 IMAGE_OPTIONAL_HEADER,但很多人称它为扩展头,意为对 PE 属性的一个扩展说明,但个人觉得叫可选头也没错,因为某些文件类型没有它,比如 obj 文件,但此头是 NT 头里面最重要的头,PE 加载器查找该头提供的特定信息,以便能够加载和运行可执行文件。

很多人有一个疑问,为什么文件头里有一个可选头大小字段说明 (IMAGE_FILE_HEADER.SizeOfOptionalHeader),原因也很简单,就是可选头的大小不固定。可选头有两个版本,一个 32 位的,一个 64 位的,这两种版本有两个不同之处:

  • 结构体本身的大小,IMAGE_OPTIONAL_HEADER32 有 31 个成员,而 IMAGE_OPTIONAL_HEADER64 只有 30 个成员,少掉的一个字段为 32 位版本中的 BaseOfData。32 位空间有限,需要找到数据段的地址,64 位取消此字段,空间更大,单独用区块表来描述数据,不再需要该字段进行定位;
  • 部分成员的数据类型:以下 5 个字段在 32 位中定义为 DWORD ,在 64 位中定义为 ULONGLONG (ImageBase、SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit);

结构如下

// 32 位
typedef struct _IMAGE_OPTIONAL_HEADER {
    // 标准域
    WORD    Magic;							//[0x18] 1) 标志位
    BYTE    MajorLinkerVersion;  			//[0x1A] 2) 链接器主版本号
    BYTE    MinorLinkerVersion;  			//[0x1B] 3) 链接器子版本号
    DWORD   SizeOfCode;   					//[0x1C] 4) 所有代码段
    DWORD   SizeOfInitializedData;  		//[0x20] 5) 所有已初始化段总大小
    DWORD   SizeOfUninitializedData; 		//[0x24] 6) 所有未初始化段总大小
    DWORD   AddressOfEntryPoint; 			//[0x28] 7) 程序执行入口 RVA
    DWORD   BaseOfCode;  					//[0x2C] 8) 代码段起始 RVA
    DWORD   BaseOfData;  					//[0x30] 9) 数据段起始 RVA

    // NT 附加域
    DWORD   ImageBase;  					//[0x34] 10) 程序默认载入基地址
    DWORD   SectionAlignment;  				//[0x38] 11) 内存中的段对齐值
    DWORD   FileAlignment;  				//[0x3C] 12) 文件中的段对齐值
    WORD    MajorOperatingSystemVersion;	//[0x40] 13) 系统主版本号
    WORD    MinorOperatingSystemVersion;  	//[0x42] 14) 系统子版本号
    WORD    MajorImageVersion;  			//[0x44] 15) 自定义的主版本号
    WORD    MinorImageVersion;  			//[0x46] 16) 自定义的子版本号
    WORD    MajorSubsystemVersion;  		//[0x48] 17) 所需子系统主版本号
    WORD    MinorSubsystemVersion;  		//[0x4A] 18) 所需子系统子版本号
    DWORD   Win32VersionValue;  			//[0x4C] 19) 保留,通常为 0x00
    DWORD   SizeOfImage;  					//[0x50] 20) 内存中映像总尺寸
    DWORD   SizeOfHeaders;  				//[0x54] 21) 各个文件头的总尺寸
    DWORD   CheckSum;  						//[0x58] 22) 映像文件校验和
    WORD    Subsystem;  					//[0x5C] 23) 文件子系统
    WORD    DllCharacteristics;  			//[0x5E] 24) DLL 标志位
    DWORD   SizeOfStackReserve; 			//[0x60] 25) 初始化栈大小
    DWORD   SizeOfStackCommit;  			//[0x64] 26) 初始化实际提交栈大小
    DWORD   SizeOfHeapReserve; 				//[0x68] 27) 初始化保留栈大小
    DWORD   SizeOfHeapCommit;  				//[0x6C] 28) 初始化实际保留栈大小
    DWORD   LoaderFlags;  					//[0x70] 29) 调试相关,默认 0x00
    DWORD   NumberOfRvaAndSizes;  			//[0x74] 30) 数据目录表的数量
    IMAGE_DATA_DIRECTORY DataDirectory[0x10]; //[0x78] 31) 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

// 64 位
typedef struct _IMAGE_OPTIONAL_HEADER64 {
    // 标准域
    WORD    Magic;							//[0x18] 1) 标志位
    BYTE    MajorLinkerVersion;  			//[0x1A] 2) 链接器主版本号
    BYTE    MinorLinkerVersion;  			//[0x1B] 3) 链接器子版本号
    DWORD   SizeOfCode;   					//[0x1C] 4) 所有代码段
    DWORD   SizeOfInitializedData;  		//[0x20] 5) 所有已初始化段总大小
    DWORD   SizeOfUninitializedData; 		//[0x24] 6) 所有未初始化段总大小
    DWORD   AddressOfEntryPoint; 			//[0x28] 7) 程序执行入口 RVA
    DWORD   BaseOfCode;  					//[0x2C] 8) 代码段起始 RVA

    // DWORD   BaseOfData;  				//[0x30] 9) 数据段起始 RVA

    // NT 附加域
    ULONGLONG ImageBase;  					//[0x34] 9) 程序默认载入基地址
    DWORD   SectionAlignment;  				//[0x38] 10) 内存中的段对齐值
    DWORD   FileAlignment;  				//[0x3C] 11) 文件中的段对齐值
    WORD    MajorOperatingSystemVersion;	//[0x40] 12) 系统主版本号
    WORD    MinorOperatingSystemVersion;  	//[0x42] 13) 系统子版本号
    WORD    MajorImageVersion;  			//[0x44] 14) 自定义的主版本号
    WORD    MinorImageVersion;  			//[0x46] 15) 自定义的子版本号
    WORD    MajorSubsystemVersion;  		//[0x48] 16) 所需子系统主版本号
    WORD    MinorSubsystemVersion;  		//[0x4A] 17) 所需子系统子版本号
    DWORD   Win32VersionValue;  			//[0x4C] 18) 保留,通常为 0x00
    DWORD   SizeOfImage;  					//[0x50] 19) 内存中映像总尺寸
    DWORD   SizeOfHeaders;  				//[0x54] 20) 各个文件头的总尺寸
    DWORD   CheckSum;  						//[0x58] 21) 映像文件校验和
    WORD    Subsystem;  					//[0x5C] 22) 文件子系统
    WORD    DllCharacteristics;  			//[0x5E] 23) DLL 标志位
    ULONGLONG  SizeOfStackReserve; 			//[0x60] 24) 初始化栈大小
    ULONGLONG  SizeOfStackCommit;  			//[0x68] 25) 初始化实际提交栈大小
    ULONGLONG  SizeOfHeapReserve; 			//[0x70] 26) 初始化保留栈大小
    ULONGLONG  SizeOfHeapCommit;  			//[0x78] 27) 初始化实际保留栈大小
    DWORD   LoaderFlags;  					//[0x80] 28) 调试相关,默认 0x00
    DWORD   NumberOfRvaAndSizes;  			//[0x84] 29) 数据目录表的数量
    IMAGE_DATA_DIRECTORY DataDirectory[0x10]; //[0x88] 30) 数据目录表
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR64_MAGIC
#else
typedef IMAGE_OPTIONAL_HEADER32             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR32_MAGIC
#endif

同样只提相对比较重要的字段,Magic 字段表示文件的类型,普通可执行镜像为 0x010B (用宏 IMAGE_NT_OPTIONAL_HDR32_MAGIC 表示),ROM 镜像为  0x0107,PE32+ 可执行文件为 0x20B (用宏 IMAGE_NT_OPTIONAL_HDR64_MAGIC 表示),这个字段决定了可执行文件是 32 位的还是 64 位的,Windows 加载器将忽略 IMAGE_FILE_HEADER.Machine

PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeaders->OptionalHeader;
if (pOptionalHeader->Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
	printf("Magic: PE32\n");
}
else if (pOptionalHeader->Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
	printf("Magic: PE32+ (64-bit)\n");
}
else if (pOptionalHeader->Magic == IMAGE_ROM_OPTIONAL_HDR_MAGIC) {
	printf("Magic: ROM\n");
}

SizeOfCode 字段表示所有 IMAGE_SCN_CNT_CODE 属性 (SCN 是 Section 缩写,CNT 是 Contains 缩写,意为包含代码的区段) 的区段总大小,此大小在计算时按照磁盘扇区字节数的整数倍 (文件对齐粒度 0x200) 计算。

SizeOfInitializedData 字段保存初始化数据 (.data) 大小,如果有多个部分,则保存所有初始化数据部分 (已初始化的全局变量和静态变量) 的总和,属性用宏 IMAGE_SCN_CNT_INITIALIZED_DATA 表示。

SizeOfUninitializedData 字段保存未初始化数据 (.bss) 大小,如果有多个部分,则保存所有未初始化数据部分 (未初始化的全局变量和静态变量) 的总和,属性用宏 IMAGE_SCN_CNT_UNINITIALIZED_DATA 表示。

AddressOfEntryPoint 字段表示程序执行入口的 RVA,在大多数可执行文件中,入口点并不指向 main、winmain 和 dllmain 等函数的入口,而是指向运行库代码,再由其调用这些函数。对于程序映像,此相对地址指向起始地址,对于设备驱动程序,它指向初始化函数。对于 DLL,入口点是可选的,如果没有入口点,则 AddressOfEntryPoint 字段设置为 0。

BaseOfCode 字段表示代码段起始 RVA,起始基地址 0x6B0000,.text 为 0x6B1000

BaseOfCode

BaseOfData 字段 (仅 32 位) 表示数据段的起始 RVA。起始基地址 0x6B0000,.rdata 为 0x6CD000

BaseOfData

ImageBase 字段表示文件在内存中的首选装入的基地址 (默认为 0x400000),加载器将试图在此地址载入,如果载入成功,则跳过基址重定位步骤,如果此地址被占用,则会重新在正确对齐的合法地址中选择一个作为实际载入地址,由于 ASLR 等内存保护 (VS 中默认开启,ASLR 需要操作系统支持及程序本身支持,两者满足才能生效,这个生效条件一定要记住,xp 系统不支持 ASLR) 以及许多其他原因该字段指定的地址几乎从未被使用过,在这种情况下,PE 加载器会选择一个未使用的内存范围来加载映像,将映像加载到该地址后加载器进入一个称为重定位的过程,有一个特殊的部分保存有关如果需要重定位则需要修复位置的信息,该部分称为重定位表 (信息位于 .reloc 区段中)。

什么是 ASLR ?

ASLR (Address space layout randomization,地址空间布局随机化) 是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。据研究表明 ASLR 可以有效降低缓冲区溢出攻击的成功率,如今 Linux、FreeBSD、Windows 等主流操作系统都已采用了该技术。

把文件载入到调试器中,发现载入的首地址为 0x6B5C68,开头第一个不是 4,这个就是 ASLR 在起作用

将文件载入到调试器中

如果没有 ASLR,载入的首地址可以计算出 ImageBase + AddressOfEntryPoint = 0x400000 + 0x5C68 = 0x405C68,而不是 0x6B5C68

AddressOfEntryPoint 与 ImageBase

在 ASLR 存在的情况下,每次重启操作系统后 (必须重启,载入首地址才会发生变化) 将文件载入调试器查看,载入首地址会变化,细心的你有没有发现其中规律,那就是载入的首地址低 2 个字节无论如何都不会发生变化 (想想为什么,其实很简单)。那如何去掉 ASLR 呢,有两种方法 (这里只从文件角度出发,通过修改注册表来关闭系统 ASLR 就不说了),一种是在有源码情况下只需要将随机基址选项设置为否即可:

Visual Studio 中关闭随机基址

另一种是没有源码情况下,即修改他人编写的程序,通过修改文件头中 Characteristics 字段中 IMAGE_FILE_RELOCS_STRIPPED 值为 1 即可 (stripped 意为剥离)

IMAGE_FILE_RELOCS_STRIPPED

或者修改可选头中 DllCharacteristics 字段中 IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 的值为 0

IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE

在关掉 ASLR 后,重新载入到调试器中查看,发现载入的首地址变为 0x405C68 了

关闭 ASLR 后再载入到调试器

ImageBase 可谓是比较重要的字段,花了这么大的篇幅,接下来说下其它字段吧。

SectionAlignment 字段表示内存对齐粒度,默认为 0x1000。

FileAlignment 字段表示文件对齐粒度,默认为 0x200。

SizeOfImage 字段表示文件装入内存后的总大小 (从 ImageBase 到最后一个区段的总大小),它会向上舍入 SectionAligment 的倍数,因为将映像加载到内存时会使用该值。

加起来等于 SizeOfImage 大小

0x1000 + 0x1C000 + 0xA000 + 0x2000 + 0x1000 = 0x2A000

这还可以通过区段结构中 VirtualSize 计算,不过这个字段并未做对齐处理,按 0x1000 对齐后处理后也是能计算的出,请自行计算验证。

DIE 中对此文件区段解析的数据

SizeOfHeader 字段表示 MS-DOS 头、PE 头和区块目录表的大小之和,为 FileAlignment 的倍数 (0x200)。010 Editor 中 PE 解析模板好像不会将 Rich Header 结尾的 0 大小给计算到 DosStub 里去

SizeOfHeaders

CheckSum 字段表示文件的校验和 (在一些 exe 文件中此值可为 0,但在一些内核模式的驱动与系统 DLL 中此值必须为一个有效值)。

Subsystem 字段表示可执行文件所期望的子系统值,这个值只对 exe 文件是重要的。

Subsystem 可能取的值

DllCharacteristics 字段定义可执行映像文件的一些特征,例如是否兼容 NX 以及是否可以在运行时重定位。我不知道为什么它被命名为 DllCharacteristics ,它存在于正常的可执行映像文件中,并且定义了可应用于正常可执行文件的特征,文档说明 https://learn.microsoft.com/zh-cn/windows/win32/debug/pe-format#dll-characteristics

SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve 和 SizeOfHeapCommit 这些字段指定保留堆栈的大小、提交堆栈的大小,保留本地堆空间的大小和提交本地堆空间的大小。

NumberOfRvaAndSizes 字段表示目录成员的数量,一般为 16 (最后一个目录成员值为 0)。

DataDirectory 字段表示 IMAGE_DATA_DIRECTORY 结构的数组,结构如下

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress; // 数据块的起始 RVA 地址
    DWORD   Size;           // 数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

这是一个非常简单的结构,只有两个字段,第一个为数据目录起始 RVA,第二个为数据目录的大小。

什么是数据目录,数据目录是位于 PE 文件的一个部分中的一段数据。数据目录包含加载程序所需的有用信息,一个非常重要的目录例子是导入表目录,它包含从其它库导入的外部函数的列表,后面会专门来介绍它。

请注意,并非所有数据目录都具有相同的结构,IMAGE_DATA_DIRECTORY.VirualAddress 指向数据目录,但是该目录的类型决定了如何解析该数据块。

以下为 winnt.h 中定义的数据目录表

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0x0   //[0x78] 1)  导出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT          0x1   //[0x80] 2)  导入表
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        0x2   //[0x88] 3)  资源表
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       0x3   //[0x90] 4)  异常表
#define IMAGE_DIRECTORY_ENTRY_SECURITY        0x4   //[0x98] 5)  安全表
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       0x5   //[0xA0] 6)  重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG           0x6   //[0xA8] 7)  调试信息表
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    0x7   //[0xB0] 8)  版权信息表
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       0x8   //[0xB8] 9)  全局指针偏移表
#define IMAGE_DIRECTORY_ENTRY_TLS             0x9   //[0xC0] 10) TLS 表
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG     0xA   //[0xC8] 11) 加载配置表
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT    0xB   //[0xD0] 12) 绑定导入表
#define IMAGE_DIRECTORY_ENTRY_IAT             0xC   //[0xD8] 13) IAT 表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT    0xD   //[0xE0] 14) 延迟导入表
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR  0xE   //[0xE8] 15) COM 描述

后面会专门来介绍数据目录表这个问题

#include <time.h>

VOID ParseNTHeader(PIMAGE_NT_HEADERS pNtHeaders) {
	// ............

	PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeaders->OptionalHeader;
	if (pOptionalHeader->Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
		printf("Magic: PE32\n");
	}
	else if (pOptionalHeader->Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
		printf("Magic: PE32+ (64-bit)\n");
	}
    else if (pOptionalHeader->Magic == IMAGE_ROM_OPTIONAL_HDR_MAGIC) {
	    printf("Magic: ROM\n");
    }

	printf("SizeOfCode: 0x%08X\n", pOptionalHeader->SizeOfCode);
	printf("SizeOfInitializedData: 0x%08X\n", pOptionalHeader->SizeOfInitializedData);
	printf("SizeOfUninitializedData: 0x%08X\n", pOptionalHeader->SizeOfUninitializedData);
	printf("AddressOfEntryPoint: 0x%08X\n", pOptionalHeader->AddressOfEntryPoint);
	printf("BaseOfCode: 0x%08X\n", pOptionalHeader->BaseOfCode);
	printf("BaseOfData: 0x%08X\n", pOptionalHeader->BaseOfData);
	printf("ImageBase: 0x%08X\n", pOptionalHeader->ImageBase);
	printf("SectionAlignment: 0x%08X\n", pOptionalHeader->SectionAlignment);
	printf("FileAlignment: 0x%08X\n", pOptionalHeader->FileAlignment);
	printf("SizeOfImage: 0x%08X\n", pOptionalHeader->SizeOfImage);
	printf("SizeOfHeaders: 0x%08X\n", pOptionalHeader->SizeOfHeaders);
	printf("CheckSum: 0x%08X\n", pOptionalHeader->CheckSum);
	switch (pOptionalHeader->Subsystem) {
	case IMAGE_SUBSYSTEM_NATIVE:
		printf("Subsystem: Native\n");
		break;
	case IMAGE_SUBSYSTEM_WINDOWS_GUI:
		printf("Subsystem: Windows GUI\n");
		break;
	case IMAGE_SUBSYSTEM_WINDOWS_CUI:
		printf("Subsystem: Windows CUI\n");
		break;
	case IMAGE_SUBSYSTEM_OS2_CUI:
		printf("Subsystem: OS/2 CUI\n");
		break;
	case IMAGE_SUBSYSTEM_POSIX_CUI:
		printf("Subsystem: POSIX CUI\n");
		break;
	case IMAGE_SUBSYSTEM_NATIVE_WINDOWS:
		printf("Subsystem: Native Windows\n");
		break;
	case IMAGE_SUBSYSTEM_WINDOWS_CE_GUI:
		printf("Subsystem: Windows CE GUI\n");
		break;
	default:
		printf("Subsystem: Unknown\n");
		break;
	}
	printf("DllCharacteristics: 0x%04X\n", pOptionalHeader->DllCharacteristics);
	printf("SizeOfStackReserve: 0x%08X\n", pOptionalHeader->SizeOfStackReserve);
	printf("SizeOfStackCommit: 0x%08X\n", pOptionalHeader->SizeOfStackCommit);
	printf("SizeOfHeapReserve: 0x%08X\n", pOptionalHeader->SizeOfHeapReserve);
	printf("SizeOfHeapCommit: 0x%08X\n", pOptionalHeader->SizeOfHeapCommit);
	printf("LoaderFlags: 0x%08X\n", pOptionalHeader->LoaderFlags);
	printf("NumberOfRvaAndSizes: %d\n", pOptionalHeader->NumberOfRvaAndSizes);
	const char* directoryType[] = {
		"Export", "Import", "Resource", "Exception", "Security", "BaseRelocation",
		"Debug", "Copyright", "GlobalPtr", "TLS", "LoadConfig", "BoundImport",
		"ImportAddressTable", "DelayImportDescriptor", "COMDescriptor", "Reserved"
	};
	printf("DataDirectory:\n");
	for (int i = 0; i < IMAGE_NUMBEROF_DIRECTORY_ENTRIES; i++) {
		printf("  [%-21s] RVA: 0x%08X, Size: 0x%08X\n", directoryType[i],
			pOptionalHeader->DataDirectory[i].VirtualAddress,
			pOptionalHeader->DataDirectory[i].Size);
	}
}

运行后 (解析的是 kernel32.dll)

解析 kernel32.dll 文件

最后来回顾下 NT 头的总体结构 (32 位),我画了一张图,方便大家脑海里有个大概样子:

NT 头的总体结构 (32 位)

区段目录

在前两节中,我们讨论了 DOS 头和 NT 头。在这一节当中将讨论区段部分 (位于数据目录表后),这部分可看作区段的一个汇总目录,描述了各个区段的属性:

区段目录索引

PE 文件最少要一个区段才能被加载运行,区段表由数个首尾相连的 IMAGEE_SECTION_HEADER 结构体组成的一个数组,可以使用 IMAGE_FIRST_SECTION(NtHeader) 这个宏找到第一个区段所在的位置,winnt.h 中对此定义

IMAGE_SECTION_HEADER 宏定义

此宏只需提供一个 NT 头结构参数即可,定义宏的上面有一条注释,意思为该宏不需要 32 或 64 位版本,我记得早期此结构有 32 位版,注释也说明了原因,无论 32 位的还是 64 位的,文件头都是相同的,区段结构体如下

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[0x8];               // 1) 区段名
    union {
        DWORD   PhysicalAddress;
        DWORD   VirtualSize;
    } Misc;                         // 2) 区段大小
    DWORD   VirtualAddress;         // 3) 区段的 RVA 地址
    DWORD   SizeOfRawData;          // 4) 文件中的区段对齐大小
    DWORD   PointerToRawData;       // 5) 区段在文件中的偏移
    DWORD   PointerToRelocations;   // 6) 重定位的偏移(用于 OBJ 文件)
    DWORD   PointerToLinenumbers;   // 7) 行号表的偏移(用于调试)
    WORD    NumberOfRelocations;    // 8) 重定位表项数量(用于 OBJ 文件)
    WORD    NumberOfLinenumbers;    // 9) 行号表项数量
    DWORD   Characteristics;        // 10) 区段的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

FIELD_OFFSET 宏定义

FIELD_OFFSET 定义

根据注释可知该宏的功能是计算一个结构体中某个字段的偏移。

对于区段结构体也只说重要字段,Name 字段是一个为 8 字节的 ASCII 字符串数组,一般情况下以 “.” 开始,但这并不是必须的 (以 $ 开头的同名区段会被合并),区段名称是可以自定义的,但微软对区段名称还是有一个约定俗成命名标准

区段名称说明

区段名一般是由编译器在自动编译链接时生成的,不过可以使用以下代码来自定义数据区段名

// #pragma data_seg("secton_name");

// for example
#pragma data_seg("zer0day")
int z = 1;
自行添加的 zer0day 区段
  • VirtualSize 字段为实际使用的段大小,需要注意的是此字段未做对齐处理,在 obj 文件中为 0。
  • VirtualAddress 区段载入到内存后的 RVA,这个地址是做了对齐处理的 (内存对齐粒度)。
  • SizeOfRawData 字段表示此区段在文件中大小,也是做了对齐处理 (文件对齐粒度),它与 VirtualSize 可能不同,发生这种情况原因有多种,当该区段加载到内存时,它不会遵循文件对齐方式,仅占用该区段的实际大小,在这种情况下,SizeOfRawData 将大于 VirtualSize,相反的情况也可能发生,如果该区段包含未初始化数据,这些数据将不会被记录在磁盘上,但是当该区段被映射到内存中时,该区段将扩展以预留内存空间以便稍后初始化和使用未初始化的数据,这说明了磁盘上的区段占用空间将小于内存中区段占用空间,在这种情况下 VirtualSize 将大于 SizeOfRawData。
  • PointerToRawData 字段表示区段在文件中的偏移。
  • PointerToRelocations 字段表示重定位表的偏移地址,指向 IMAGE_RELOCATION 结构体数组。
  • Characteristics 字段表示区段的属性,描述此区段的读写情况、状态等属性
Characteristics 字段可能的值

属性值可以用 “|” 进行合并,例如 0xE0000020 就是 “0x20000000 | 0x40000000 | 0x80000000 | 0x00000020” 几种属性合并后的结果,表示的是这是一个可读、可写、可执行的区段。以下为代码解析

VOID ParseSectionTable(PIMAGE_NT_HEADERS pNtHeaders) {
	printf("==================== Section ====================\n");
	printf("Name\tVirtualSize\tVirtualAddress\tSizeOfRawData\tPointerToRawData\tPointerToRelocations\tCharacteristics\n");
	PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeaders);
	DWORD dwSectionCount = pNtHeaders->FileHeader.NumberOfSections;
	for (DWORD dwIndex = 0; dwIndex < dwSectionCount; dwIndex++) {
		printf("%s\t", pSectionHeader->Name);
		printf("0x%08X\t", pSectionHeader->Misc.VirtualSize);
		printf("0x%08X\t", pSectionHeader->VirtualAddress);
		printf("0x%08X\t", pSectionHeader->SizeOfRawData);
		printf("0x%08X\t\t", pSectionHeader->PointerToRawData);
		printf("0x%08X\t\t", pSectionHeader->PointerToRelocations);
		printf("0x%08X\n", pSectionHeader->Characteristics);
		pSectionHeader++;
	}
}

运行结果与 DIE 检测结果作比较

结果对比

区段为什么要对齐

因为这是微软规定的东西,每个区段与结构的起始位置都要遵循页对齐机制,在 32 位平台下,一个分页的大小为 4KB,所以无论是在内存中还是在文件中对齐,一般为 4KB 的整数倍 (但 PE 文件对此并无强制性规定,只要是 2 的整数倍即可),剩余有空多出的空间全部以 0 填充。

RVA 和 FOA 相互转换

RVA (相对虚拟地址) 到 FOA (file offset address,文件偏移地址) 的相互转换,这个东西挺重要的,需要记住。

计算公式

FOA = VA – ImageBase – (所在区段的 RVA – 所在区段的 FOA) 或 FOA = RVA – 所在区段的 RVA + 所在区段的 FOA 

common.h

#pragma once

#include <Windows.h>

DWORD Rva2Foa(PIMAGE_NT_HEADERS pNtHeader, DWORD dwRva);
DWORD Foa2Rva(PIMAGE_NT_HEADERS pNtHeader, DWORD dwFoa);

common.c

#include "common.h"

// RVA 转 FOA
DWORD Rva2Foa(PIMAGE_NT_HEADERS pNtHeader, DWORD dwRva) {
    // 获取区段表的数量
    DWORD dwSectionNumber = pNtHeader->FileHeader.NumberOfSections;

    // 获取区段表数组的首元素
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);

    // 遍历所有的区段表找到符合要求的区段
    for (DWORD dwIndex = 0; dwIndex < dwSectionNumber; ++dwIndex)
    {
        // 要求:RVA >= 区段的首地址并且 RVA < 区段的结尾的地址
        if (dwRva >= pSectionHeader->VirtualAddress &&
            dwRva < (pSectionHeader->VirtualAddress + pSectionHeader->SizeOfRawData))
        {
            // FOA = VA - ImageBase - (所在区段的 RVA - 所在区段的 FOA)
            // FOA = RVA - 所在区段的 RVA + 所在区段的 FOA
            return dwRva - pSectionHeader->VirtualAddress + pSectionHeader->PointerToRawData;
        }
        pSectionHeader++;
    }

    // 如果找不到就返回 FALSE
    return FALSE;
}

// FOA 转 RVA
DWORD Foa2Rva(PIMAGE_NT_HEADERS pNtHeader, DWORD dwFoa) {
    // 获取区段表的数量
    DWORD dwSectionNumber = pNtHeader->FileHeader.NumberOfSections;

    // 获取区段表数组的首元素
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);

    // 遍历所有的区段表找到符合要求的区段
    for (DWORD dwIndex = 0; dwIndex < dwSectionNumber; ++dwIndex)
    {
        // 要求:FOA >= 区段的首地址并且 FOA < 区段的结尾的地址
        if (dwFoa >= pSectionHeader->PointerToRawData &&
            dwFoa < (pSectionHeader->PointerToRawData + pSectionHeader->SizeOfRawData))
        {
            // RVA = FOA - 所在区段的 FOA + 所在区段的 RVA
            return dwFoa - pSectionHeader->PointerToRawData + pSectionHeader->VirtualAddress;
        }
        pSectionHeader++;
    }

    // 如果找不到就返回 FALSE
    return FALSE;
}

导出目录表

导出目录表一般出现在 Dll 文件中,由于前面采用的示例未有导出目录表,为了做演示,便拿系统中 kernel32.dll 来做解析。

导出目录表是 PE 文件为其它程序提供 API 的一种函数示例导出方式,除此之外,还可导出自身一些变量以及类,供第三方程序使用,导出目录表结构

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;          // 1) 保留,恒为 0x00000000
    DWORD   TimeDateStamp;            // 2) 时间戳
    WORD    MajorVersion;             // 3) 主版本号,一般不赋值
    WORD    MinorVersion;             // 4) 子版本号,一般不赋值
    DWORD   Name;                     // 5) 模块名称
    DWORD   Base;                     // 6) 索引基数
    DWORD   NumberOfFunctions;        // 7) 导出地址表中的成员个数
    DWORD   NumberOfNames;            // 8) 导出名称表中的成员个数
    DWORD   AddressOfFunctions;       // 9) 导出地址表(EAT)
    DWORD   AddressOfNames;           // 10) 导出名称表(ENT)
    DWORD   AddressOfNameOrdinals;    // 11) 指向导出序列号数组 (EOT)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • Characteristics 字段恒为 0,保留;
  • TimeDateStamp 字段为导出目录表创建的时间 (GMT);
  • Name 字段为存储模块名 (ASCII 字符) 的 RVA;
  • Base 字段为导出 API 函数索引值的基数,函数索引值 = 导出函数索引值 – 基数,一般情况下为 0;
  • NumberOfFunctions 字段为导出地址表 (EAT) 中成员数量;
  • NumberOfNames 字段为导出名称表 (ENT) 中成员数量,ENT 的数量 <= EAT 的数量;
  • AddressOfFunctions 字段为导出地址表的 RVA;
  • AddressOfNames 字段为导出名称表的 RVA;
  • AddressOfNameOrdinals 字段为导出序列号数组的 RVA;

有三张表非常重要,分别为导出地址表 EAT、导出名称表 ENT、导出序号表 EOT,三张表中,三者的关系:

导出地址表 EAT、导出名称表 ENT、导出序号表 EOT 三者关系

函数导出有两种方式,一种仅以序号导出,另一种是同时以序号和名称导出。需要注意的是,EAT 的数量会大于等于 ENT 的数量。

LIBRARY "Dll1"
EXPORTS
  fun1 @1 NONAME
  fun2 @2 NONAME
  fun3 @3 NONAME

LIBRARY "Dll1"
EXPORTS
  fun1 @1 
  fun2 @2 
  fun3 @3 NONAME

kernel32.dll 中导出目录表的 RVA 为 0x92CA0,大小为 0xDC60,位于 .rdata 区段中,转换成文件偏移为 0x78CA0

kernel32.dll 中的导出目录表 RVA 与 FOA 之间的转换

在 010Editor 里也验证了这个结果

010 Editor 里验证结果

代码实现:

VOID ParseExportTable(PIMAGE_NT_HEADERS pNtHeaders, PUINT8 buffer) {
	printf("==================== Export ====================\n");
	DWORD dwExportTableRVA = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
	DWORD dwExportTableSize = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

	if (dwExportTableRVA == 0 || dwExportTableSize == 0) {
		printf("无导出表\n");
		return;
	}

	PIMAGE_EXPORT_DIRECTORY pExportTableVA = (PIMAGE_EXPORT_DIRECTORY)(buffer +
		Rva2Foa(pNtHeaders, dwExportTableRVA));

	printf("Name: %s\n", (char*)(buffer + Rva2Foa(pNtHeaders, pExportTableVA->Name)));

	WORD* pwOrdTable = (WORD*)(buffer + Rva2Foa(pNtHeaders, pExportTableVA->AddressOfNameOrdinals));
	DWORD* pdwNamesTable = (DWORD*)(buffer + Rva2Foa(pNtHeaders, pExportTableVA->AddressOfNames));
	DWORD* pdwFunctionsTable = (DWORD*)(buffer + Rva2Foa(pNtHeaders, pExportTableVA->AddressOfFunctions));

	printf("Ordinal\tRVA\t\tNames\n");

	for (DWORD i = 0; i < pExportTableVA->NumberOfFunctions; ++i) {
		BOOL bHaveName = FALSE;
		// 查看当前函数有没有名称
		for (DWORD j = 0; j < pExportTableVA->NumberOfNames; ++j) {
			// 如果序号表中保存了当前地址的下标,说明是名称导出
			// i 表示当前地址下标,j 是名称表和序号表的下标
			if (i == pwOrdTable[j]) {
				bHaveName = TRUE;
				printf("0x%04X\t0x%08X\t%s",
					i + pExportTableVA->Base, pdwFunctionsTable[i],
					(char*)(buffer + Rva2Foa(pNtHeaders, pdwNamesTable[j])));
				// 判断是否有转发
				if (pdwFunctionsTable[i] >= dwExportTableRVA &&
					pdwFunctionsTable[i] < (dwExportTableRVA + dwExportTableSize)) {
					printf(" (forwarded to %s)\n", (char*)(buffer + Rva2Foa(pNtHeaders, pdwFunctionsTable[i])));
				}
				else {
					printf("\n");
				}
			}
		}
		// 这是一个序号导出的函数
		if (!bHaveName) {
			printf("0x%04X\t0x%08X\t[None]",
				i + pExportTableVA->Base, pdwFunctionsTable[i]);
			if (pdwFunctionsTable[i] >= dwExportTableRVA &&
				pdwFunctionsTable[i] < (dwExportTableRVA + dwExportTableSize)) {
				printf(" (forwarded to %s)\n", (char*)(buffer + Rva2Foa(pNtHeaders, pdwFunctionsTable[i])));
			}
			else {
				printf("\n");
			}
		}
	}
}

运行后如下:

kernel32.dll 中的导出目录表

GetProcAddress 函数是个典型例子,向它提供模块名和函数名就能获取到函数地址,自己可以去实现这样一个函数。

最后以一张图理清楚其中的关系

导出目录表一图理清关系

导入目录表

导入目录表为 PE 文件从其它第三库中导入 API,供本程序使用,根据导入目录表能猜测出程序的大致行为,导入目录表在 PE 文件结构中也是相当重要,应用程序调用了系统中的某些函数,那么这些信息就会体现在导入目录表中。导入目录表结构体

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;  // 1) 指向导入名称表 (INT) 的 RVA
    };
    DWORD   TimeDateStamp;           // 2) 时间标识
    DWORD   ForwarderChain;          // 3) 转发链,如果不转发则此值为 0
    DWORD   Name;                    // 4) 指向导入映像文件的名字
    DWORD   FirstThunk;              // 5) 指向导入地址表 (IAT) 的 RVA
} IMAGE_IMPORT_DESCRIPTOR;

这个结构起到引导作用,引导系统找到真正保存有导入信息的其它两个结构,这两个结构为 IMAGE_THUNK_DATA 和 IMAGE_IMPORT_BY_NAME,有多少个导入映像,就有多少个 IMAGE_IMPORT_DESCRIPTOR 结构,最后以一个空的 IMAGE_IMPORT_DESCRIPTOR 结构结束。

  • OriginalFirstThunk 字段指向 INT 的 RVA,INT 是一个 IMAGE_THUNK_DATA 结构数组,数组中的每个 IMAGE_THUNK_DATA 结构会再指向 IMAGE_IMPORT_BY_NAME 结构,结尾为一个全 0 的空 IMAGE_THUNK_DATA 结构结束;
  • ForwarderChain 字段为导入表转发器 forwarders 的索引值,一个映像文件可以输出一个没有在本文件内定义的符号,并且这个符号可以是从另一个映像文件引入的,这样的符号称为转发符号,当此值为 -1 时,说明此文件转发已结束,当为 0 时,证明此映像文件未启用此机制;
  • Name 字段为导入映像名称;
  • FirstThunk 字段为导入地址表 IAT 的 RVA;

这个结构体只需要关注两个字段,分别为第一个和最后一个,第一个保存的是导入名称表 (INT) 的 RVA,第二个保存的是导入地址表 (IAT) 的 RVA,这两个都指向为 IMAGE_THUNK_DATA 结构数组,IMAGE_THUNK_DATA 结构如下

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        PBYTE  ForwarderString;                 // 1) 转发字符串的 RVA
        PDWORD Function;                        // 2) 被导入函数的地址
        DWORD Ordinal;                          // 3) 被导入函数的序号
        PIMAGE_IMPORT_BY_NAME  AddressOfData;   // 4) 导入名称表 RVA
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

此结构里只有 u1 成员,且为联合体结构,ForwarderString 字段负责与导入表转发器 forwarders 协同工作,当导入表的 ForwarderChain 不为 0 时,此值有效,并指向包含有转发函数与导出这个函数的映像文件名的字符串 RVA。

  • Function 字段为导入表导入函数的实际内存地址,此字段仅在映像被加载,且此结构为 IAT 的前提下有效。
  • Ordinal 字段为导入表导入函数的导出序号,当 IMAGE_THUNK_DATA 的最高位为 1 时,此值有效。
  • AddressOfData 字段为指向 IMAGE_IMPORT_BY_NAME 结构,当以上 3 个值都未生效时,此值有效。

微软规定当 IMAGE_THUNK_DATA 最高位为 1 时,就采用序号导入方式,而且此时这个值的低 31 位将被看作是一个函数序号。

在导出目录表一节当中知道函数导出的方式有两种,一种为仅以序号导出,另一个是以序号和名称一块导出,同样,当以仅序号导入时,IMAGE_THUNK_DATA 最高位为 1,当以序号和名称一块导入时,是不是最高位也为 1?一个 32 位的数最高为 1,这个数最小为 0x80000000,在 windows 内存中,0x80000000 以上的空间被称为系统空间,仅系统可用,因此程序中的有效访问地址总是小于 0x80000000,这也就有效解决了序号导入方式与名称导入方式可能会发生碰撞问题,这设计的妙啊。

IMAGE_SNAP_BY_ORDINAL 这个宏可以判断此项是否为序号,参数为 Ordinal

#ifdef _WIN64
#define IMAGE_ORDINAL_FLAG              IMAGE_ORDINAL_FLAG64
#define IMAGE_ORDINAL(Ordinal)          IMAGE_ORDINAL64(Ordinal)
typedef IMAGE_THUNK_DATA64              IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA64             PIMAGE_THUNK_DATA;
#define IMAGE_SNAP_BY_ORDINAL(Ordinal)  IMAGE_SNAP_BY_ORDINAL64(Ordinal)
typedef IMAGE_TLS_DIRECTORY64           IMAGE_TLS_DIRECTORY;
typedef PIMAGE_TLS_DIRECTORY64          PIMAGE_TLS_DIRECTORY;
#else
#define IMAGE_ORDINAL_FLAG              IMAGE_ORDINAL_FLAG32
#define IMAGE_ORDINAL(Ordinal)          IMAGE_ORDINAL32(Ordinal)
typedef IMAGE_THUNK_DATA32              IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA32             PIMAGE_THUNK_DATA;
#define IMAGE_SNAP_BY_ORDINAL(Ordinal)  IMAGE_SNAP_BY_ORDINAL32(Ordinal)
typedef IMAGE_TLS_DIRECTORY32           IMAGE_TLS_DIRECTORY;
typedef PIMAGE_TLS_DIRECTORY32          PIMAGE_TLS_DIRECTORY;
#endif

#define IMAGE_ORDINAL_FLAG64 0x8000000000000000
#define IMAGE_ORDINAL_FLAG32 0x80000000
#define IMAGE_SNAP_BY_ORDINAL64(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG64) != 0)
#define IMAGE_SNAP_BY_ORDINAL32(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG32) != 0)

在这里再说下 Function 字段,在 PE 文件未被系统加载之前,INT 和 IAT 都是使用 AddressOfData 字段指向 IMAGE_IMPORT_BY_NAME 结构,当加载后,操作系统先会遍历 INT 中的内容,并逐一取出已导入函数的内存地址,然后将这些动态获取的地址逐一填入到 IAT 中,此时操作系统使用的是 Function 字段,IMAGE_IMPORT_BY_NAME 结构

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;    // 1) 需导入的函数序号
    BYTE    Name[1]; // 2) 需导入的函数名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Hint 字段保存的是序号,Name 字段保存的是一个不定长的字符串,以下图为描述整个导入目录表结构的情况

导入目录表一图理清关系

代码实现

VOID ParseImportTable(PIMAGE_NT_HEADERS pNtHeaders, PUINT8 buffer) {
	printf("==================== Import ====================\n");
	DWORD pImportTableRVA = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
	DWORD dwImportTableSize = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;

	if (pImportTableRVA == 0 || dwImportTableSize == 0) {
		printf("无导入表\n");
		return;
	}

	PIMAGE_IMPORT_DESCRIPTOR pImportTableVA = (PIMAGE_IMPORT_DESCRIPTOR)(buffer +
		Rva2Foa(pNtHeaders, pImportTableRVA));
	// 开始遍历所有导入的 Dll
	while (pImportTableVA->Name != 0) {
		// 解析被导入的模块名称
		printf("Name: %s\n", (char*)(buffer + Rva2Foa(pNtHeaders, pImportTableVA->Name)));

		// INT: 不管是在内存中还是在文件中,通常保存的是函数的名称
		// IAT: 在文件中通常保存的是函数的名称,在内存中会被修复成函数的地址
		PIMAGE_THUNK_DATA pINTable = (PIMAGE_THUNK_DATA)(buffer +
			Rva2Foa(pNtHeaders, pImportTableVA->OriginalFirstThunk));

		printf("Ordials\tName\n");

		// INT 表以一个全 0 的字段结尾
		while (pINTable->u1.Function) {
			// 函数有两种导入方式,以序号方式导入和以名称方式导入
			// 需要判断函数是否有名字
			if (IMAGE_SNAP_BY_ORDINAL(pINTable->u1.Ordinal)) {
				// 当最高位为 1 时,说明这是一个以序号导出的函数
				// 这个字段的低 2 个字节保存的是当前函数的序号
				printf("0x%04X\t[None]\n", pINTable->u1.Ordinal & 0xFFFF);
			}
			else {
				// 当这个函数是以名称方式导入时,当前字段保存的是一个 RVA
				// 指向 IMAGE_IMPORT_BY_NAME 结构体
				// 第一个成员保存的是导入函数的序号
				// 第二个成员保存的是一个不定长的字符串
				PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(buffer +
					Rva2Foa(pNtHeaders, pINTable->u1.AddressOfData));
				printf("0x%04X\t%s\n", pName->Hint, pName->Name);
			}
			// 遍历下一个函数
			pINTable++;
		}
		// 遍历下一个模块
		pImportTableVA++;
	}
}

运行结果

解析 kernel32.dll 导入目录表结果

资源目录表

在程序中,像菜单、图标、快捷键和版本信息等信息都属于资源,程序中的资源以目录结构的形式存在,一般情况下分为三层

  • 资源类型
  • 目录资源 ID
  • 资源代码页

资源目录表结构用 IMAGE_RESOURCE_DIRECTORY 表示

typedef struct _IMAGE_RESOURCE_DIRECTORY {
    DWORD   Characteristics;             // 1) 资源属性标识
    DWORD   TimeDateStamp;               // 2) 资源建立的时间
    WORD    MajorVersion;                // 3) 资源主版本
    WORD    MinorVersion;                // 4) 资源子版本
    WORD    NumberOfNamedEntries;        // 5) 资源名称条目个数
    WORD    NumberOfIdEntries;           // 6) 资源 ID 项个数
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

字段说明

  • Characteristics:资源属性,一般为 0;
  • TimeDateStamp:资源建立时间;
  • MajorVersion:资源主版本;
  • MinorVersion:资源子版本;
  • NumberOfNameEntries:用字符串作为资源标识的个数;
  • NumberOfIdEntries:用数字 ID 作为资源标识的个数;

上述结构体指出紧随其后的 IMAGE_RESOURCE_DIRECTORY_ENTRY (PIMAGE_RESOURCE_DIRECTORY + 1,见后面实现代码,解释了为什么需要加上 1) 结构数组的成员个数

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
	union {
		struct {
			DWORD NameOffset : 31;			// 1) 资源名偏移
			DWORD NameIsString : 1;			// 2) 资源名为字符串
		};
		DWORD Name;							// 3) 资源/语言类型
		WORD Id;							// 4) 资源数字 ID
	};
	union {
		DWORD OffsetToData;					// 5) 数据偏移地址
		struct {
			DWORD OffsetToDirectory : 31;	// 6) 子目录偏移地址
			DWORD DataIsDirectory : 1;		// 7) 数据为目录
		};
	};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

上述结构体是由两个大小为 4 字节的联合体组成,相关字段说明

  • NameOffset:当 NameIsString 为 1 时,此字段的值为资源名字字符串偏移;
  • NameIsString:资源名为字符串,当此值为 1 时,NameOffset 会指向一个 IMAGE_RESOURCE_DIR_STRING_U 结构体,此结构体保存着资源名称;
  • Name:当位于第一层目录中时,此字段保存资源类型的值,当位于第三层目录中时,此字段保存资源语言区域的类型值;
  • Id:资源数字 ID;
  • OffsetToData:数据 RVA;
  • OffsetToDirectory:当 DataIsDirectory 为 1 时,此字段的值指向下一层子目录的偏移 (相对资源目录起始地址偏移);
  • DataIsDirectory:数据指向目标为子目录;

IMAGE_RESOURCE_DIR_STRING_U

typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
	WORD Length;				// 1) 字符串长度
	WCHAR NameString[1];		// 2) 字符串数组
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

第一个联合体内的字段是根据当前结构体所处的目录层次来决定,位于第一层目录时 Name 有效,保存的信息是资源类型;位于第二层目录时 Id 或结构体有效,这取决于此资源的索引方式,如果采用的是编号索引就是 Id 有效,否则结构体有效;位于第三层目录时 Name 有效,保存的信息是资源语言区域类型。

第二个联合体内的字段理论上是根据具体情况而定,如果下一级是一个子目录的话,那么就是结构体有效,如果下一级是资源数据则 OffsetToData 有效。

在经过三层目录的索引后,最后会到达一个 IMAGE_RESOURCE_DATA_ENTRY 结构中,这个结构将指引我们找到资源数据

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD   OffsetToData;    // 1) 资源数据的 RVA
    DWORD   Size;            // 2) 资源数据的长度
    DWORD   CodePage;        // 3) 代码页
    DWORD   Reserved;        // 4) 保留字段
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

字段说明

  • OffsetToData:资源数据 RVA 的指针;
  • Size:资源数据的大小;
  • CodePage:资源的代码页信息;
  • Reserved:保留字段,恒为 0;

代码实现

// 内置 ID 对应的字符串名称
// https://learn.microsoft.com/zh-cn/windows/win32/menurc/resource-types
static const char* ResType[] = {
	"", "Cursor", "Bitmap", "Icon", "Menu", "Dialog", "String Table", "Font Directory",
	"Font", "Accelerators", "Unformatted", "Message Table", "Croup Cursor", "",
	"Group Icon", "", "Version Information","Dlginclude", "", "PlugPlay", "VxD", 
	"AniCursor", "AniIcon", "HTML", "Manifest"
};

// 通过递归的方式来遍历资源目录表信息
VOID GetResTableInfo(PIMAGE_NT_HEADERS pNtHeaders, PUINT8 buffer, DWORD dwResTableVA, PIMAGE_RESOURCE_DIRECTORY pResTableVA, int iLevel) {
	// 获取内置类型和自定义类型的数量
	DWORD dwIdCounts = pResTableVA->NumberOfNamedEntries + pResTableVA->NumberOfIdEntries;

	PIMAGE_RESOURCE_DIRECTORY_ENTRY pResEntry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pResTableVA + 1);

	for (DWORD dwIndex = 0; dwIndex < dwIdCounts; ++dwIndex) {
		// 第一层保存的是资源的类型
		if (iLevel == 1) {
			// 判断是否为内置类型
			if (pResEntry[dwIndex].NameIsString) {
				// 如果最高位为 1,则表明这是一个自定义的类型
				PIMAGE_RESOURCE_DIR_STRING_U pResType = (PIMAGE_RESOURCE_DIR_STRING_U)
					(pResEntry[dwIndex].NameOffset + dwResTableVA);
				printf("ResType: %ws\n", pResType->NameString);
			}
			else {
				printf("ResType: %s\n", ResType[pResEntry[dwIndex].Id]);
			}
		}
		// 第二层保存的是资源名称或 ID
		else if (iLevel == 2) {
			// 判断这个资源是不是使用名称命名的
			if (pResEntry[dwIndex].NameIsString) {
				PIMAGE_RESOURCE_DIR_STRING_U pResName = (PIMAGE_RESOURCE_DIR_STRING_U)
					(pResEntry[dwIndex].NameOffset + dwResTableVA);
				printf("  ResName: %ws\n", pResName->NameString);
			}
			else {
				printf("  ResID: %d\n", pResEntry[dwIndex].Id);
			}
		}
		// 第三层保存的是具体的资源
		else if (iLevel == 3) {
			printf("    ResID: %ld\n", pResEntry[dwIndex].Name);
			PIMAGE_RESOURCE_DATA_ENTRY pResData = (PIMAGE_RESOURCE_DATA_ENTRY)
				(pResEntry[dwIndex].OffsetToData + dwResTableVA);
			printf("    Size: %ld\n", pResData->Size);
			DWORD dwDataFoa = Rva2Foa(pNtHeaders, pResData->OffsetToData);
			PBYTE pData = buffer + dwDataFoa;
			HexDump(pData, pResData->Size, dwDataFoa);
			printf("\n");
			break;
		}

		if (pResEntry[dwIndex].DataIsDirectory) {
			PIMAGE_RESOURCE_DIRECTORY pNextResDir = (PIMAGE_RESOURCE_DIRECTORY)
				(pResEntry[dwIndex].OffsetToDirectory + dwResTableVA);
			GetResTableInfo(pNtHeaders, buffer, dwResTableVA, pNextResDir, iLevel + 1);
		}
	}
}

VOID ParseResourcesTable(PIMAGE_NT_HEADERS pNtHeaders, PUINT8 buffer) {
	printf("==================== Resources ====================\n");
	DWORD dwResTableRva = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress;
	DWORD dwResTableSize = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size;

	if (dwResTableRva == 0 || dwResTableSize == 0) {
		printf("无资源表\n");
		return;
	}

	PIMAGE_RESOURCE_DIRECTORY pResTableVA = (PIMAGE_RESOURCE_DIRECTORY)(buffer +
		Rva2Foa(pNtHeaders, dwResTableRva));

	GetResTableInfo(pNtHeaders, buffer, (DWORD)pResTableVA, pResTableVA, 1);
}

在 common.h 中添加

VOID HexDump(const void* data, size_t size, size_t baseOffset);

common.c 中添加

VOID HexDump(const void* data, size_t size, size_t baseOffset) {
    const unsigned char* p = (const unsigned char*)data;

    for (size_t i = 0; i < size; i += 16) {
        printf("%08llX  ", (unsigned long long)(baseOffset + i));

        for (size_t j = 0; j < 16; j++) {
            if (i + j < size) {
                printf("%02X ", p[i + j]);
            }
            else {
                printf("   ");
            }
            if (j == 7) {
                printf(" ");
            }
        }

        printf(" ");

        for (size_t j = 0; j < 16 && i + j < size; j++) {
            unsigned char c = p[i + j];
            printf("%c", (c >= 0x20 && c <= 0x7E) ? c : '.');
        }

        printf("\n");
    }
}

运行结果

解析资源目录表

异常目录表

异常目录表在 32 位程序中没多大意义,以 0 填充,若要以上述代码作为解析前提,需要将相关结构转化为 64 位版,再添加如下代码解析 64 位程序中异常目录表。

以下为学习异常目录表参考资料:

在 common.h 中添加

// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-180
typedef enum _UNWIND_OP_CODES {
	UWOP_PUSH_NONVOL = 0, /* info == register number */
	UWOP_ALLOC_LARGE,     /* no info, alloc size in next 2 slots */
	UWOP_ALLOC_SMALL,     /* info == size of allocation / 8 - 1 */
	UWOP_SET_FPREG,       /* no info, FP = RSP + UNWIND_INFO.FPRegOffset*16 */
	UWOP_SAVE_NONVOL,     /* info == register number, offset in next slot */
	UWOP_SAVE_NONVOL_FAR, /* info == register number, offset in next 2 slots */
	UWOP_EPILOG,
	UWOP_SAVE_XMM128 = 8, /* info == XMM reg number, offset in next slot */
	UWOP_SAVE_XMM128_FAR, /* info == XMM reg number, offset in next 2 slots */
	UWOP_PUSH_MACHFRAME   /* info == 0: no error-code, 1: error-code */
} UNWIND_CODE_OPS;

typedef unsigned char UBYTE;

typedef union _UNWIND_CODE {
	struct {
		UBYTE CodeOffset;
		UBYTE UnwindOp : 4;
		UBYTE OpInfo : 4;
	};
	USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

typedef struct _UNWIND_INFO {
	UBYTE Version       : 3;
	UBYTE Flags         : 5;
	UBYTE SizeOfProlog;
	UBYTE CountOfCodes;
	UBYTE FrameRegister : 4;
	UBYTE FrameOffset   : 4;
	UNWIND_CODE UnwindCode[1];
	/* UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
	 * union {
	 *     OPTIONAL ULONG ExceptionHandler;
	 *     OPTIONAL ULONG FunctionEntry;
	 * };
	 * OPTIONAL ULONG ExceptionData[];
	 */
} UNWIND_INFO, *PUNWIND_INFO;

PEParser.c 中添加

VOID ParseExceptionTable(PIMAGE_NT_HEADERS64 pNtHeaders, PUINT8 buffer) {
	printf("==================== Exception ====================\n");
	DWORD dwExceptionTableRva = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress;
	DWORD dwExceptionTableSize = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size;

	if (dwExceptionTableRva == 0 || dwExceptionTableSize == 0) {
		printf("无异常表\n");
		return;
	}

	DWORD dwExceptionTableCount = dwExceptionTableSize / sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY);
	
	PRUNTIME_FUNCTION pExceptionTableVA = (PRUNTIME_FUNCTION)(buffer +
		Rva2Foa(pNtHeaders, dwExceptionTableRva));

	const char* RegName[16] = {
		"RAX", "RCX", "RDX", "RBX",
		"RSP", "RBP", "RSI", "RDI",
		"R8",  "R9",  "R10", "R11",
		"R12", "R13", "R14", "R15"
	};

	const char* XmmRegName[16] = {
		"XMM0", "XMM1", "XMM2", "XMM3",
		"XMM4", "XMM5", "XMM6", "XMM7",
		"XMM8", "XMM9", "XMM10", "XMM11",
		"XMM12", "XMM13", "XMM14", "XMM15"
	};

	for (DWORD i = 0; i < dwExceptionTableCount; ++i) {
		DWORD dwBeginRva = pExceptionTableVA[i].BeginAddress;
		DWORD dwEndRva = pExceptionTableVA[i].EndAddress;
		DWORD dwUnwindRva = pExceptionTableVA[i].UnwindData;

		printf("[%4u] Function: RVA %08X - %08X, Unwind RVA: %08X\n", i, dwBeginRva, dwEndRva, dwUnwindRva);

		PUNWIND_INFO pUnwindInfo = (PUNWIND_INFO)(buffer + Rva2Foa(pNtHeaders, dwUnwindRva));
		printf("  UnwindInfo:\n    Version: %u\n    Flags: %u\n    Prolog: 0x%X\n    Codes: %u\n    FrameReg: %u\n    FrameOff: %u\n",
			pUnwindInfo->Version,
			pUnwindInfo->Flags,
			pUnwindInfo->SizeOfProlog,
			pUnwindInfo->CountOfCodes,
			pUnwindInfo->FrameRegister,
			pUnwindInfo->FrameOffset
		);
		PUNWIND_CODE pCodes = &pUnwindInfo->UnwindCode[0];
		for (int j = 0; j < pUnwindInfo->CountOfCodes; ++j) {
			printf("        [%d] Offset: 0x%02X, Op: ", j, pCodes[j].CodeOffset);
			switch (pCodes[j].UnwindOp) {
			case UWOP_PUSH_NONVOL:
				printf("PUSH_NONVOL %s\n", RegName[pCodes[j].OpInfo]);
				break;
			case UWOP_ALLOC_LARGE:
				printf("ALLOC_LARGE ");
				if (pCodes[j].OpInfo == 0) {
					WORD size = *(WORD*)&pCodes[j + 1];
					printf(" (Size: %u bytes)\n", size * 8);
					j++;
				}
				else {
					DWORD size = *(DWORD*)&pCodes[j + 1];
					printf(" (Size: %u bytes)\n", size * 8);
					j += 2;
				}
				break;
			case UWOP_ALLOC_SMALL:
				printf("ALLOC_SMALL (Size: %u bytes)\n", (pCodes[j].OpInfo + 1) * 8);
				break;
			case UWOP_SET_FPREG:
				printf("SET_FPREG, FP = [RSP + %u]\n", pUnwindInfo->FrameRegister * 16);
				break;
			case UWOP_SAVE_NONVOL: {
				WORD disp = *(WORD*)&pCodes[j + 1];
				printf("SAVE_NONVOL %s, [RSP + %u]\n", RegName[pCodes[j].OpInfo], disp * 8);
				j++;
				break;
			}
			case UWOP_SAVE_NONVOL_FAR: {
				DWORD disp = *(DWORD*)&pCodes[j + 1];
				printf("SAVE_NONVOL_FAR %s, [RSP + %u]\n", RegName[pCodes[j].OpInfo], disp);
				j += 2;
				break;
			}
			case UWOP_SAVE_XMM128: {
				WORD disp = *(WORD*)&pCodes[j + 1];
				printf("SAVE_XMM128 %s, [RSP + %d]\n", XmmRegName[pCodes[j].OpInfo], disp);
				j++;
				break;
			}
			case UWOP_SAVE_XMM128_FAR: {
				DWORD disp = *(DWORD*)&pCodes[j + 1];
				printf("SAVE_XMM128_FAR %s, [RSP + %u]\n", XmmRegName[pCodes[j].OpInfo], disp);
				j += 2;
				break;
			}
			case UWOP_PUSH_MACHFRAME:
				printf("PUSH_MACHFRAME (Error Code: %s)\n", pCodes[j].OpInfo == 0 ? "No" : "Yes");
				break;
			default:
				printf("Unknown (0x%X)\n", pCodes[j].UnwindOp);
				break;
			}
		}

		if (pUnwindInfo->Flags & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER | UNW_FLAG_CHAININFO)) {
			ULONG codesSize = ((pUnwindInfo->CountOfCodes + 1) & ~1) * sizeof(UNWIND_CODE);
			ULONG* handlerPtr = (ULONG*)((uintptr_t)pUnwindInfo +
				offsetof(UNWIND_INFO, UnwindCode) + codesSize);

			if (pUnwindInfo->Flags & UNW_FLAG_CHAININFO) {
				printf("    ChainInfo RVA: 0x%08lX\n", *handlerPtr);
			}
			else if (pUnwindInfo->Flags & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER)) {
				printf("    Handler RVA: 0x%08lX\n", *handlerPtr);
			}
		}
	}
}

运行结果

安全目录表

安全目录表是在数据目录表中第 5 个,该表中存放着 PE 文件证书签名信息,安全目录表结构在 WinTrust.h 中定义为 WIN_CERTIFICATE 类型

typedef struct _WIN_CERTIFICATE
{
    DWORD       dwLength;
    WORD        wRevision;
    WORD        wCertificateType;   // WIN_CERT_TYPE_xxx
    BYTE        bCertificate[ANYSIZE_ARRAY];
} WIN_CERTIFICATE, *LPWIN_CERTIFICATE;

dwLength 表示不定长数组 bCertificate 大小。

wRevision 表示在 bCertificate 字段里保存证书的版本号,这个版本号有两个类型,用以下宏表示

#define WIN_CERT_REVISION_1_0               (0x0100)
#define WIN_CERT_REVISION_2_0               (0x0200)

wCertificateType 指定 bCertificate 内容类型,根据内容不同,Windows SDK 对其做了如下宏定义

#define WIN_CERT_TYPE_X509                  (0x0001)   // bCertificate contains an X.509 Certificate
#define WIN_CERT_TYPE_PKCS_SIGNED_DATA      (0x0002)   // bCertificate contains a PKCS SignedData structure
#define WIN_CERT_TYPE_RESERVED_1            (0x0003)   // Reserved
#define WIN_CERT_TYPE_TS_STACK_SIGNED       (0x0004)   // Terminal Server Protocol Stack Certificate signing

bCertificate 包含一盒证书 (里面有一个或多个证书),安全数据目录中的证书可能并非只有一个,这主要取决于第一个证书的大小与安全目录表中 Size 的值,如果安全目录表的大小大于第一个证书,那么就存在第二个证书,依此类推。

例如 kernel32.dll

kernel32.dll 数字签名证书

代码实现

PEParser.c 添加

#include <WinTrust.h>

VOID ParseSecurityTable(PIMAGE_NT_HEADERS pNtHeaders, PUINT8 buffer) {
	printf("==================== Security ====================\n");
	DWORD dwSecurityVA = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress;
	DWORD dwSecuritySize = pNtHeaders->OptionalHeader.
		DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size;

	if (dwSecurityVA == 0 || dwSecuritySize == 0) {
		printf("无安全表\n");
		return;
	}

	// 不需要转 FOA
	PUINT8 ptr = buffer + dwSecurityVA;
	int certIndex = 0;

	while (dwSecuritySize >= sizeof(WIN_CERTIFICATE)) {
		WIN_CERTIFICATE* pCert = (WIN_CERTIFICATE*)ptr;
		DWORD dwCertLen = pCert->dwLength - sizeof(WIN_CERTIFICATE);
		BYTE* pbCertData = pCert->bCertificate;
		printf("Certificate %d: length: 0x%X, Revision: 0x%04X", 
			certIndex, pCert->dwLength, pCert->wRevision);

		switch (pCert->wRevision) {
		case WIN_CERT_REVISION_1_0: {
			printf(" (Win Certificate v1.0)");
			break;
		}
		case WIN_CERT_REVISION_2_0: {
			printf(" (Win Certificate v2.0)");
			break;
		}
		default: break;
		}

		printf(", Type: 0x%04X", pCert->wCertificateType);

		switch (pCert->wCertificateType) {
		case WIN_CERT_TYPE_X509: {
			printf(" (X509)\n");
			break;
		}
		case WIN_CERT_TYPE_PKCS_SIGNED_DATA: {
			printf(" (PKCS #7)\n");
			break;
		}
		case WIN_CERT_TYPE_RESERVED_1: {
			printf("Reserved\n");
			break;
		}
		case WIN_CERT_TYPE_TS_STACK_SIGNED: {
			printf("TS Stack Signed\n");
			break;
		default: break;
		}
		}

		if (pCert->dwLength > dwSecuritySize) {
			break;
		}

		for (DWORD i = 0; i < dwCertLen; ++i) {
			if (i % 16 == 0) {
				printf("  %04X: ", i);
			}
			printf("%02X ", pbCertData[i]);
			if ((i + 1) % 16 == 0 || i + 1 == dwCertLen) {
				printf("\n");
			}
		}

		// 下一个证书
		ptr += pCert->dwLength;
		dwSecuritySize -= pCert->dwLength;
		certIndex++;
	}
}

运行如下

解析结果

若需要进一步解析 bCertificate 内容,例如解析 PKCS #7,可使用 openssl 或 crypt32 的库,请读者自行完成。

#include <wincrypt.h>

#pragma comment(lib, "crypt32.lib")

VOID ParsePKCS7Certificate(BYTE* certData, DWORD dwCertLen) {
    // ....
}

//case WIN_CERT_TYPE_PKCS_SIGNED_DATA: {
//	printf(" (PKCS #7)\n");
//	ParsePKCS7Certificate(certData, dwCertLen);
//	break;
//}

重定位目录表

重定位目录表是一张非常重要的表,在前面小结中,也提到过重定位这个概念,当 PE 文件加载到内存时,若未加载到预期的基地址 (ImageBase 决定) 处,这时就需要进行重定位操作。

一般情况下重定位表信息位于一个名为 .reloc 的区块中,PE 文件中重定位结构是由多个 IMAGE_BASE_RELOCATION 子结构组成的,每个子结构负责描述一个 4KB 大小的分布内重定位信息。重定位实质比较简单,就是比较实际加载地址与 ImageBase 的值,如果相等则不需要做任何操作,如果不相等就用其差值加上需要重定位的地址数据。因此 PE 文件中的重定位结构只负责索引出需要重定位的地址信息,并不包含具体重定位过程中可能需要的其他任何数据。

重定位结构

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

VirtualAddress 指向需要重定位数据的 RVA,由于每个重定位结构体只负责描述 0x1000 字节大小区域的重定位信息,因此这个字段的值总是 0x1000 的倍数。

SizeOfBlock 描述 IMAGE_BASE_RELOCATION 结构体与重定位数组 TypeOffset 的体积总大小。

TypeOffset 表示一个不定长的 WORD 型数组,从上述 SDK 给出的描述来看,其本身并不属于 IMAGE_BASE_RELOCATION 结构体,此数组负责与 IMAGE_BASE_RELOCATION 结构体配合描述需要进行重定位的数据具体偏移,结构如下 (SDK 中无此结构)

typedef struct _TYPEOFFSET {
	// 12 位能表示的最大值是 0xFFF,足以表示一个分页中的所有偏移
	WORD Offset : 12;
	// 这个字段通常保存的是 3,也只需要关注为 3 的字段
	WORD Type : 4;
} TYPEOFFSET;

Offset 记录位于 VirtualAddress 字段所指分页中的需要重定位的地址,这个 TypeOffset 的 2 字节结构可以非常高效地引导我们在制定分页内找到需要重定位的数据。由于在 x86 平台上最常用的索引类型为 IMAGE_REL_BASED_HIGHLOW,因此后面分析的例子也将以此为基础。

Type 表示重定位信息类型值,具体为以下宏表示

#define IMAGE_REL_BASED_ABSOLUTE              0    // 无重定位操作
#define IMAGE_REL_BASED_HIGH                  1    // 重定位偏移指向位置的高 2 字节需要被修正
#define IMAGE_REL_BASED_LOW                   2    // 重定位偏移指向位置的低 2 字节需要被修正
#define IMAGE_REL_BASED_HIGHLOW               3    // 重定位偏移指向的整个 4 字节大小的地址都需要被修正
#define IMAGE_REL_BASED_HIGHADJ               4    // 此方式需要使用两项 TypeOffset 才能完成索引工作
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_5    5
#define IMAGE_REL_BASED_RESERVED              6    // 保留
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_7    7
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_8    8
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_9    9
#define IMAGE_REL_BASED_DIR64                 10   // 重定位偏移指向的位置的 8 字节 (64 位) 地址需要被修正

以解析 kernel32.dll 为例,重定位的项数可由以下公式计算得到

重定位项数 = (pRelocVA->SizeOfBlock + sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

重定位后的地址计算公式

重定位后的地址 = (加载基址 - ImageBase) + 重定位前的地址

代码实现

PEParser.c 中添加

VOID ParseRelocTable(PIMAGE_NT_HEADERS pNtHeaders, PUINT8 buffer) {
	printf("==================== Reloc ====================\n");

	DWORD dwRelocRva = pNtHeaders->OptionalHeader
		.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
	DWORD dwRelocSize = pNtHeaders->OptionalHeader
		.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;

	if (dwRelocRva == 0 || dwRelocSize == 0) {
		printf("无重定位表\n");
		return;
	}

	PIMAGE_BASE_RELOCATION pRelocVA = (PIMAGE_BASE_RELOCATION)(buffer + 
		Rva2Foa(pNtHeaders, dwRelocRva));

	// 因为重定位块里面有一个字段是 SizeofBlock 保存的是重定位块的大小
	// 如果这个字段为 0,说明重定位快遍历结束了
	while (pRelocVA->SizeOfBlock) {
		// 重定位块[重定位结构体[8] + TypeOffsetArray]
		// 重定位块 1 + 重定位块 2 + 重定位块 3 + 00000000
		// 输出当前是哪一个分页需要进行重定位
		printf("PageRva = 0x%08X, SizeOfBlock = 0x%08X\n", 
			pRelocVA->VirtualAddress, pRelocVA->SizeOfBlock);

		PWORD pwTypeOffsets =
			(PWORD)((PUINT8)pRelocVA + sizeof(IMAGE_BASE_RELOCATION));

		// 获取重定位项 TypeOffset 的个数
		// 原因是 SizeOfBlock 是一个重定位块的大小,包含了结构体以及数组
		// - sizeof(RelocTable) 减去的是结构体的大小,/sizeof(WORD) 原因是数组元素的大小为 sizeof(WORD)
		DWORD dwCounts = (pRelocVA->SizeOfBlock + sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

		// 遍历出每一个重定位项
		for (DWORD i = 0; i < dwCounts; ++i)
		{
			WORD wRaw = pwTypeOffsets[i];
			WORD wType = (wRaw >> 12) & 0xF;
			WORD wOffset = wRaw & 0x0FFF;
			
			// 算出需要重定位的数据所在的 RVA
			DWORD dwRva = wOffset + pRelocVA->VirtualAddress;
			
			// 判断类型是否为 IMAGE_REL_BASED_HIGHLOW
			if (wType == IMAGE_REL_BASED_HIGHLOW)
			{
				// 输出重定位的信息
				printf("    TYPEOFFSET: %X%03X - TYPE: %s - RVA: %08X\n", 
					wType, wOffset, "HIGHLOW", dwRva);
			}

			if (wType == IMAGE_REL_BASED_ABSOLUTE) {
				printf("    TYPEOFFSET: %X%03X - TYPE: %s - RVA: %08X\n",
					wType, wOffset, "ABSOLUTE", dwRva);
			}
		}

		// 获取下一个重定位块
		pRelocVA = (PIMAGE_BASE_RELOCATION)
			((PUINT8)pRelocVA + pRelocVA->SizeOfBlock);
	}
}

运行后结果

解析重定位表结果

参考

  • 《黑客免杀攻防》—— 任晓珲